gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-exchange] branch master updated: Revert "[lib] delete some files"


From: gnunet
Subject: [taler-exchange] branch master updated: Revert "[lib] delete some files"
Date: Tue, 24 Oct 2023 09:22:50 +0200

This is an automated email from the git hooks/post-receive script.

grothoff pushed a commit to branch master
in repository exchange.

The following commit(s) were added to refs/heads/master by this push:
     new aee7774d Revert "[lib] delete some files"
aee7774d is described below

commit aee7774d2c99739fe4029e930ecfe9be368876f4
Author: Christian Grothoff <christian@grothoff.org>
AuthorDate: Tue Oct 24 09:22:43 2023 +0200

    Revert "[lib] delete some files"
    
    This reverts commit deabb1e506ae678eeec608649388a082ada7c11a.
---
 src/lib/auditor_api_curl_defaults.c                |   61 ++
 src/lib/auditor_api_curl_defaults.h                |   38 +
 src/lib/auditor_api_deposit_confirmation.c         |  418 ++++++++
 src/lib/auditor_api_exchanges.c                    |  244 +++++
 src/lib/auditor_api_get_config.c                   |  288 +++++
 src/lib/exchange_api_age_withdraw.c                | 1117 ++++++++++++++++++++
 src/lib/exchange_api_age_withdraw_reveal.c         |  471 +++++++++
 src/lib/exchange_api_auditor_add_denomination.c    |  238 +++++
 src/lib/exchange_api_csr_melt.c                    |  317 ++++++
 src/lib/exchange_api_kyc_check.c                   |  329 ++++++
 src/lib/exchange_api_kyc_proof.c                   |  217 ++++
 src/lib/exchange_api_kyc_wallet.c                  |  230 ++++
 src/lib/exchange_api_lookup_aml_decision.c         |  419 ++++++++
 src/lib/exchange_api_lookup_aml_decisions.c        |  378 +++++++
 src/lib/exchange_api_management_add_partner.c      |  218 ++++
 src/lib/exchange_api_management_auditor_disable.c  |  219 ++++
 src/lib/exchange_api_management_auditor_enable.c   |  224 ++++
 src/lib/exchange_api_management_drain_profits.c    |  213 ++++
 src/lib/exchange_api_management_post_extensions.c  |  213 ++++
 src/lib/exchange_api_management_post_keys.c        |  237 +++++
 ...change_api_management_revoke_denomination_key.c |  221 ++++
 .../exchange_api_management_revoke_signing_key.c   |  211 ++++
 src/lib/exchange_api_management_set_global_fee.c   |  235 ++++
 src/lib/exchange_api_management_set_wire_fee.c     |  227 ++++
 .../exchange_api_management_update_aml_officer.c   |  229 ++++
 src/lib/exchange_api_management_wire_disable.c     |  220 ++++
 src/lib/exchange_api_management_wire_enable.c      |  245 +++++
 src/lib/exchange_api_melt.c                        |  596 +++++++++++
 src/lib/exchange_api_recoup.c                      |  369 +++++++
 src/lib/exchange_api_recoup_refresh.c              |  363 +++++++
 src/lib/exchange_api_refresh_common.c              |  249 +++++
 src/lib/exchange_api_refresh_common.h              |  201 ++++
 src/lib/exchange_api_refreshes_reveal.c            |  531 ++++++++++
 src/lib/exchange_api_refund.c                      |  480 +++++++++
 src/lib/exchange_api_stefan.c                      |  320 ++++++
 src/lib/test_stefan.c                              |  206 ++++
 36 files changed, 10992 insertions(+)

diff --git a/src/lib/auditor_api_curl_defaults.c 
b/src/lib/auditor_api_curl_defaults.c
new file mode 100644
index 00000000..81fcd7ba
--- /dev/null
+++ b/src/lib/auditor_api_curl_defaults.c
@@ -0,0 +1,61 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2014-2018 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/auditor_api_curl_defaults.c
+ * @brief curl easy handle defaults
+ * @author Florian Dold
+ */
+#include "auditor_api_curl_defaults.h"
+
+
+CURL *
+TALER_AUDITOR_curl_easy_get_ (const char *url)
+{
+  CURL *eh;
+  struct GNUNET_AsyncScopeSave scope;
+
+  GNUNET_async_scope_get (&scope);
+
+  eh = curl_easy_init ();
+  if (NULL == eh)
+    return NULL;
+  GNUNET_assert (CURLE_OK ==
+                 curl_easy_setopt (eh,
+                                   CURLOPT_URL,
+                                   url));
+  GNUNET_assert (CURLE_OK ==
+                 curl_easy_setopt (eh,
+                                   CURLOPT_FOLLOWLOCATION,
+                                   1L));
+  /* Enable compression (using whatever curl likes), see
+     https://curl.se/libcurl/c/CURLOPT_ACCEPT_ENCODING.html  */
+  GNUNET_break (CURLE_OK ==
+                curl_easy_setopt (eh,
+                                  CURLOPT_ACCEPT_ENCODING,
+                                  ""));
+  /* limit MAXREDIRS to 5 as a simple security measure against
+     a potential infinite loop caused by a malicious target */
+  GNUNET_assert (CURLE_OK ==
+                 curl_easy_setopt (eh,
+                                   CURLOPT_MAXREDIRS,
+                                   5L));
+  GNUNET_assert (CURLE_OK ==
+                 curl_easy_setopt (eh,
+                                   CURLOPT_TCP_FASTOPEN,
+                                   1L));
+  return eh;
+}
diff --git a/src/lib/auditor_api_curl_defaults.h 
b/src/lib/auditor_api_curl_defaults.h
new file mode 100644
index 00000000..99e1e07e
--- /dev/null
+++ b/src/lib/auditor_api_curl_defaults.h
@@ -0,0 +1,38 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2014-2018 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/auditor_api_curl_defaults.h
+ * @brief curl easy handle defaults
+ * @author Florian Dold
+ */
+#ifndef _TALER_CURL_DEFAULTS_H
+#define _TALER_CURL_DEFAULTS_H
+
+#include "platform.h"
+#include <gnunet/gnunet_curl_lib.h>
+
+
+/**
+ * Get a curl handle with the right defaults
+ * for the auditor lib.  In the future, we might manage a pool of connections 
here.
+ *
+ * @param url URL to query
+ */
+CURL *
+TALER_AUDITOR_curl_easy_get_ (const char *url);
+
+#endif /* _TALER_CURL_DEFAULTS_H */
diff --git a/src/lib/auditor_api_deposit_confirmation.c 
b/src/lib/auditor_api_deposit_confirmation.c
new file mode 100644
index 00000000..1e2ecc6c
--- /dev/null
+++ b/src/lib/auditor_api_deposit_confirmation.c
@@ -0,0 +1,418 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2014-2023 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/auditor_api_deposit_confirmation.c
+ * @brief Implementation of the /deposit request of the auditor's HTTP API
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <jansson.h>
+#include <microhttpd.h> /* just for HTTP status codes */
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_util.h"
+#include "taler_curl_lib.h"
+#include "taler_json_lib.h"
+#include "taler_auditor_service.h"
+#include "taler_signatures.h"
+#include "auditor_api_curl_defaults.h"
+
+
+/**
+ * @brief A DepositConfirmation Handle
+ */
+struct TALER_AUDITOR_DepositConfirmationHandle
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Context for #TEH_curl_easy_post(). Keeps the data that must
+   * persist for Curl to make the upload.
+   */
+  struct TALER_CURL_PostContext ctx;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_AUDITOR_DepositConfirmationResultCallback cb;
+
+  /**
+   * Closure for @a cb.
+   */
+  void *cb_cls;
+
+};
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /deposit-confirmation request.
+ *
+ * @param cls the `struct TALER_AUDITOR_DepositConfirmationHandle`
+ * @param response_code HTTP response code, 0 on error
+ * @param djson parsed JSON result, NULL on error
+ */
+static void
+handle_deposit_confirmation_finished (void *cls,
+                                      long response_code,
+                                      const void *djson)
+{
+  const json_t *json = djson;
+  struct TALER_AUDITOR_DepositConfirmationHandle *dh = cls;
+  struct TALER_AUDITOR_DepositConfirmationResponse dcr = {
+    .hr.reply = json,
+    .hr.http_status = (unsigned int) response_code
+  };
+
+  dh->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    dcr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    break;
+  case MHD_HTTP_OK:
+    dcr.hr.ec = TALER_EC_NONE;
+    break;
+  case MHD_HTTP_BAD_REQUEST:
+    dcr.hr.ec = TALER_JSON_get_error_code (json);
+    dcr.hr.hint = TALER_JSON_get_error_hint (json);
+    /* This should never happen, either us or the auditor is buggy
+       (or API version conflict); just pass JSON reply to the application */
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    dcr.hr.ec = TALER_JSON_get_error_code (json);
+    dcr.hr.hint = TALER_JSON_get_error_hint (json);
+    /* Nothing really to verify, auditor says one of the signatures is
+       invalid; as we checked them, this should never happen, we
+       should pass the JSON reply to the application */
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    dcr.hr.ec = TALER_JSON_get_error_code (json);
+    dcr.hr.hint = TALER_JSON_get_error_hint (json);
+    /* Nothing really to verify, this should never
+       happen, we should pass the JSON reply to the application */
+    break;
+  case MHD_HTTP_GONE:
+    dcr.hr.ec = TALER_JSON_get_error_code (json);
+    dcr.hr.hint = TALER_JSON_get_error_hint (json);
+    /* Nothing really to verify, auditor says one of the signatures is
+       invalid; as we checked them, this should never happen, we
+       should pass the JSON reply to the application */
+    break;
+  case MHD_HTTP_INTERNAL_SERVER_ERROR:
+    dcr.hr.ec = TALER_JSON_get_error_code (json);
+    dcr.hr.hint = TALER_JSON_get_error_hint (json);
+    /* Server had an internal issue; we should retry, but this API
+       leaves this to the application */
+    break;
+  default:
+    /* unexpected response code */
+    dcr.hr.ec = TALER_JSON_get_error_code (json);
+    dcr.hr.hint = TALER_JSON_get_error_hint (json);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for auditor deposit 
confirmation\n",
+                (unsigned int) response_code,
+                dcr.hr.ec);
+    break;
+  }
+  dh->cb (dh->cb_cls,
+          &dcr);
+  TALER_AUDITOR_deposit_confirmation_cancel (dh);
+}
+
+
+/**
+ * Verify signature information about the deposit-confirmation.
+ *
+ * @param h_wire hash of merchant wire details
+ * @param h_policy hash over the policy extension, if any
+ * @param h_contract_terms hash of the contact of the merchant with the 
customer (further details are never disclosed to the auditor)
+ * @param exchange_timestamp timestamp when the deposit was received by the 
wallet
+ * @param wire_deadline by what time must the amount be wired to the merchant
+ * @param refund_deadline date until which the merchant can issue a refund to 
the customer via the auditor (can be zero if refunds are not allowed); must not 
be after the @a wire_deadline
+ * @param amount_without_fee the amount confirmed to be wired by the exchange 
to the merchant
+ * @param coin_pub coin’s public key
+ * @param merchant_pub the public key of the merchant (used to identify the 
merchant for refund requests)
+ * @param exchange_sig the signature made with purpose 
#TALER_SIGNATURE_EXCHANGE_CONFIRM_DEPOSIT
+ * @param exchange_pub the public key of the exchange that matches @a 
exchange_sig
+ * @param master_pub master public key of the exchange
+ * @param ep_start when does @a exchange_pub validity start
+ * @param ep_expire when does @a exchange_pub usage end
+ * @param ep_end when does @a exchange_pub legal validity end
+ * @param master_sig master signature affirming validity of @a exchange_pub
+ * @return #GNUNET_OK if signatures are OK, #GNUNET_SYSERR if not
+ */
+static enum GNUNET_GenericReturnValue
+verify_signatures (
+  const struct TALER_MerchantWireHashP *h_wire,
+  const struct TALER_ExtensionPolicyHashP *h_policy,
+  const struct TALER_PrivateContractHashP *h_contract_terms,
+  struct GNUNET_TIME_Timestamp exchange_timestamp,
+  struct GNUNET_TIME_Timestamp wire_deadline,
+  struct GNUNET_TIME_Timestamp refund_deadline,
+  const struct TALER_Amount *amount_without_fee,
+  unsigned int num_coins,
+  const struct TALER_CoinSpendSignatureP *coin_sigs[
+    static num_coins],
+  const struct TALER_MerchantPublicKeyP *merchant_pub,
+  const struct TALER_ExchangePublicKeyP *exchange_pub,
+  const struct TALER_ExchangeSignatureP *exchange_sig,
+  const struct TALER_MasterPublicKeyP *master_pub,
+  struct GNUNET_TIME_Timestamp ep_start,
+  struct GNUNET_TIME_Timestamp ep_expire,
+  struct GNUNET_TIME_Timestamp ep_end,
+  const struct TALER_MasterSignatureP *master_sig)
+{
+  if (GNUNET_OK !=
+      TALER_exchange_online_deposit_confirmation_verify (
+        h_contract_terms,
+        h_wire,
+        h_policy,
+        exchange_timestamp,
+        wire_deadline,
+        refund_deadline,
+        amount_without_fee,
+        num_coins,
+        coin_sigs,
+        merchant_pub,
+        exchange_pub,
+        exchange_sig))
+  {
+    GNUNET_break_op (0);
+    TALER_LOG_WARNING (
+      "Invalid signature on /deposit-confirmation request!\n");
+    {
+      TALER_LOG_DEBUG ("... amount_without_fee was %s\n",
+                       TALER_amount2s (amount_without_fee));
+    }
+    return GNUNET_SYSERR;
+  }
+
+  if (GNUNET_OK !=
+      TALER_exchange_offline_signkey_validity_verify (
+        exchange_pub,
+        ep_start,
+        ep_expire,
+        ep_end,
+        master_pub,
+        master_sig))
+  {
+    GNUNET_break (0);
+    TALER_LOG_WARNING ("Invalid signature on exchange signing key!\n");
+    return GNUNET_SYSERR;
+  }
+  if (GNUNET_TIME_absolute_is_past (ep_end.abs_time))
+  {
+    GNUNET_break (0);
+    TALER_LOG_WARNING ("Exchange signing key is no longer valid!\n");
+    return GNUNET_SYSERR;
+  }
+  return GNUNET_OK;
+}
+
+
+struct TALER_AUDITOR_DepositConfirmationHandle *
+TALER_AUDITOR_deposit_confirmation (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  const struct TALER_MerchantWireHashP *h_wire,
+  const struct TALER_ExtensionPolicyHashP *h_policy,
+  const struct TALER_PrivateContractHashP *h_contract_terms,
+  struct GNUNET_TIME_Timestamp exchange_timestamp,
+  struct GNUNET_TIME_Timestamp wire_deadline,
+  struct GNUNET_TIME_Timestamp refund_deadline,
+  const struct TALER_Amount *total_without_fee,
+  unsigned int num_coins,
+  const struct TALER_CoinSpendPublicKeyP *coin_pubs[
+    static num_coins],
+  const struct TALER_CoinSpendSignatureP *coin_sigs[
+    static num_coins],
+  const struct TALER_MerchantPublicKeyP *merchant_pub,
+  const struct TALER_ExchangePublicKeyP *exchange_pub,
+  const struct TALER_ExchangeSignatureP *exchange_sig,
+  const struct TALER_MasterPublicKeyP *master_pub,
+  struct GNUNET_TIME_Timestamp ep_start,
+  struct GNUNET_TIME_Timestamp ep_expire,
+  struct GNUNET_TIME_Timestamp ep_end,
+  const struct TALER_MasterSignatureP *master_sig,
+  TALER_AUDITOR_DepositConfirmationResultCallback cb,
+  void *cb_cls)
+{
+  struct TALER_AUDITOR_DepositConfirmationHandle *dh;
+  json_t *deposit_confirmation_obj;
+  CURL *eh;
+  json_t *jcoin_sigs;
+  json_t *jcoin_pubs;
+
+  if (0 == num_coins)
+  {
+    GNUNET_break (0);
+    return NULL;
+  }
+  if (GNUNET_OK !=
+      verify_signatures (h_wire,
+                         h_policy,
+                         h_contract_terms,
+                         exchange_timestamp,
+                         wire_deadline,
+                         refund_deadline,
+                         total_without_fee,
+                         num_coins,
+                         coin_sigs,
+                         merchant_pub,
+                         exchange_pub,
+                         exchange_sig,
+                         master_pub,
+                         ep_start,
+                         ep_expire,
+                         ep_end,
+                         master_sig))
+  {
+    GNUNET_break_op (0);
+    return NULL;
+  }
+  jcoin_sigs = json_array ();
+  GNUNET_assert (NULL != jcoin_sigs);
+  jcoin_pubs = json_array ();
+  GNUNET_assert (NULL != jcoin_pubs);
+  for (unsigned int i = 0; i<num_coins; i++)
+  {
+    GNUNET_assert (0 ==
+                   json_array_append_new (jcoin_sigs,
+                                          GNUNET_JSON_from_data_auto (
+                                            coin_sigs[i])));
+    GNUNET_assert (0 ==
+                   json_array_append_new (jcoin_pubs,
+                                          GNUNET_JSON_from_data_auto (
+                                            coin_pubs[i])));
+  }
+  deposit_confirmation_obj
+    = GNUNET_JSON_PACK (
+        GNUNET_JSON_pack_data_auto ("h_wire",
+                                    h_wire),
+        GNUNET_JSON_pack_data_auto ("h_policy",
+                                    h_policy),
+        GNUNET_JSON_pack_data_auto ("h_contract_terms",
+                                    h_contract_terms),
+        GNUNET_JSON_pack_timestamp ("exchange_timestamp",
+                                    exchange_timestamp),
+        GNUNET_JSON_pack_allow_null (
+          GNUNET_JSON_pack_timestamp ("refund_deadline",
+                                      refund_deadline)),
+        GNUNET_JSON_pack_timestamp ("wire_deadline",
+                                    wire_deadline),
+        TALER_JSON_pack_amount ("total_without_fee",
+                                total_without_fee),
+        GNUNET_JSON_pack_array_steal ("coin_pubs",
+                                      jcoin_pubs),
+        GNUNET_JSON_pack_array_steal ("coin_sigs",
+                                      jcoin_sigs),
+        GNUNET_JSON_pack_data_auto ("merchant_pub",
+                                    merchant_pub),
+        GNUNET_JSON_pack_data_auto ("exchange_sig",
+                                    exchange_sig),
+        GNUNET_JSON_pack_data_auto ("master_pub",
+                                    master_pub),
+        GNUNET_JSON_pack_timestamp ("ep_start",
+                                    ep_start),
+        GNUNET_JSON_pack_timestamp ("ep_expire",
+                                    ep_expire),
+        GNUNET_JSON_pack_timestamp ("ep_end",
+                                    ep_end),
+        GNUNET_JSON_pack_data_auto ("master_sig",
+                                    master_sig),
+        GNUNET_JSON_pack_data_auto ("exchange_pub",
+                                    exchange_pub));
+  dh = GNUNET_new (struct TALER_AUDITOR_DepositConfirmationHandle);
+  dh->cb = cb;
+  dh->cb_cls = cb_cls;
+  dh->url = TALER_url_join (url,
+                            "deposit-confirmation",
+                            NULL);
+  if (NULL == dh->url)
+  {
+    GNUNET_free (dh);
+    return NULL;
+  }
+  eh = TALER_AUDITOR_curl_easy_get_ (dh->url);
+  if ( (NULL == eh) ||
+       (CURLE_OK !=
+        curl_easy_setopt (eh,
+                          CURLOPT_CUSTOMREQUEST,
+                          "PUT")) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&dh->ctx,
+                              eh,
+                              deposit_confirmation_obj)) )
+  {
+    GNUNET_break (0);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    json_decref (deposit_confirmation_obj);
+    GNUNET_free (dh->url);
+    GNUNET_free (dh);
+    return NULL;
+  }
+  json_decref (deposit_confirmation_obj);
+  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+              "URL for deposit-confirmation: `%s'\n",
+              dh->url);
+  dh->job = GNUNET_CURL_job_add2 (ctx,
+                                  eh,
+                                  dh->ctx.headers,
+                                  &handle_deposit_confirmation_finished,
+                                  dh);
+  {
+    /* Disable 100 continue processing */
+    struct curl_slist *x_headers;
+
+    x_headers = curl_slist_append (NULL,
+                                   "Expect:");
+    GNUNET_CURL_extend_headers (dh->job,
+                                x_headers);
+    curl_slist_free_all (x_headers);
+  }
+  return dh;
+}
+
+
+void
+TALER_AUDITOR_deposit_confirmation_cancel (
+  struct TALER_AUDITOR_DepositConfirmationHandle *deposit_confirmation)
+{
+  if (NULL != deposit_confirmation->job)
+  {
+    GNUNET_CURL_job_cancel (deposit_confirmation->job);
+    deposit_confirmation->job = NULL;
+  }
+  GNUNET_free (deposit_confirmation->url);
+  TALER_curl_easy_post_finished (&deposit_confirmation->ctx);
+  GNUNET_free (deposit_confirmation);
+}
+
+
+/* end of auditor_api_deposit_confirmation.c */
diff --git a/src/lib/auditor_api_exchanges.c b/src/lib/auditor_api_exchanges.c
new file mode 100644
index 00000000..897dfe60
--- /dev/null
+++ b/src/lib/auditor_api_exchanges.c
@@ -0,0 +1,244 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2014-2018 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/auditor_api_exchanges.c
+ * @brief Implementation of the /exchanges request of the auditor's HTTP API
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <jansson.h>
+#include <microhttpd.h> /* just for HTTP status codes */
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_json_lib.h"
+#include "taler_auditor_service.h"
+#include "taler_util.h"
+#include "taler_curl_lib.h"
+#include "taler_signatures.h"
+#include "auditor_api_curl_defaults.h"
+
+/**
+ * How many exchanges do we allow a single auditor to
+ * audit at most?
+ */
+#define MAX_EXCHANGES 1024
+
+
+/**
+ * @brief A ListExchanges Handle
+ */
+struct TALER_AUDITOR_ListExchangesHandle
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_AUDITOR_ListExchangesResultCallback cb;
+
+  /**
+   * Closure for @a cb.
+   */
+  void *cb_cls;
+
+};
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /exchanges request.
+ *
+ * @param cls the `struct TALER_AUDITOR_ListExchangesHandle`
+ * @param response_code HTTP response code, 0 on error
+ * @param djson parsed JSON result, NULL on error
+ */
+static void
+handle_exchanges_finished (void *cls,
+                           long response_code,
+                           const void *djson)
+{
+  const json_t *json = djson;
+  const json_t *ja;
+  unsigned int ja_len;
+  struct TALER_AUDITOR_ListExchangesHandle *leh = cls;
+  struct TALER_AUDITOR_ListExchangesResponse ler = {
+    .hr.reply = json,
+    .hr.http_status = (unsigned int) response_code
+  };
+
+  leh->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    ler.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    break;
+  case MHD_HTTP_OK:
+    ja = json_object_get (json,
+                          "exchanges");
+    if ( (NULL == ja) ||
+         (! json_is_array (ja)) )
+    {
+      GNUNET_break (0);
+      ler.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+      ler.hr.http_status = 0;
+      break;
+    }
+
+    ja_len = json_array_size (ja);
+    if (ja_len > MAX_EXCHANGES)
+    {
+      GNUNET_break (0);
+      ler.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+      ler.hr.http_status = 0;
+      break;
+    }
+    {
+      struct TALER_AUDITOR_ExchangeInfo ei[GNUNET_NZL (ja_len)];
+      bool ok = true;
+
+      for (unsigned int i = 0; i<ja_len; i++)
+      {
+        struct GNUNET_JSON_Specification spec[] = {
+          GNUNET_JSON_spec_fixed_auto ("master_pub",
+                                       &ei[i].master_pub),
+          GNUNET_JSON_spec_string ("exchange_url",
+                                   &ei[i].exchange_url),
+          GNUNET_JSON_spec_end ()
+        };
+
+        if (GNUNET_OK !=
+            GNUNET_JSON_parse (json_array_get (ja,
+                                               i),
+                               spec,
+                               NULL, NULL))
+        {
+          GNUNET_break_op (0);
+          ok = false;
+          ler.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+          ler.hr.http_status = 0;
+          break;
+        }
+      }
+      if (! ok)
+        break;
+      ler.details.ok.ei = ei;
+      ler.details.ok.num_exchanges = ja_len;
+      leh->cb (leh->cb_cls,
+               &ler);
+      TALER_AUDITOR_list_exchanges_cancel (leh);
+      return;
+    }
+  case MHD_HTTP_BAD_REQUEST:
+    /* This should never happen, either us or the auditor is buggy
+       (or API version conflict); just pass JSON reply to the application */
+    ler.hr.ec = TALER_JSON_get_error_code (json);
+    ler.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    /* Nothing really to verify, this should never
+       happen, we should pass the JSON reply to the application */
+    ler.hr.ec = TALER_JSON_get_error_code (json);
+    ler.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_INTERNAL_SERVER_ERROR:
+    /* Server had an internal issue; we should retry, but this API
+       leaves this to the application */
+    ler.hr.ec = TALER_JSON_get_error_code (json);
+    ler.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  default:
+    /* unexpected response code */
+    ler.hr.ec = TALER_JSON_get_error_code (json);
+    ler.hr.hint = TALER_JSON_get_error_hint (json);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for auditor list-exchanges 
request\n",
+                (unsigned int) response_code,
+                (int) ler.hr.ec);
+    GNUNET_break_op (0);
+    break;
+  }
+  if (NULL != leh->cb)
+    leh->cb (leh->cb_cls,
+             &ler);
+  TALER_AUDITOR_list_exchanges_cancel (leh);
+}
+
+
+struct TALER_AUDITOR_ListExchangesHandle *
+TALER_AUDITOR_list_exchanges (struct GNUNET_CURL_Context *ctx,
+                              const char *url,
+                              TALER_AUDITOR_ListExchangesResultCallback cb,
+                              void *cb_cls)
+{
+  struct TALER_AUDITOR_ListExchangesHandle *leh;
+  CURL *eh;
+
+  leh = GNUNET_new (struct TALER_AUDITOR_ListExchangesHandle);
+  leh->cb = cb;
+  leh->cb_cls = cb_cls;
+  leh->url = TALER_url_join (url,
+                             "exchanges",
+                             NULL);
+  if (NULL == leh->url)
+  {
+    GNUNET_free (leh);
+    return NULL;
+  }
+  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+              "URL for list-exchanges: `%s'\n",
+              leh->url);
+  eh = TALER_AUDITOR_curl_easy_get_ (leh->url);
+  if (NULL == eh)
+  {
+    GNUNET_break (0);
+    GNUNET_free (leh->url);
+    GNUNET_free (leh);
+    return NULL;
+  }
+  leh->job = GNUNET_CURL_job_add (ctx,
+                                  eh,
+                                  &handle_exchanges_finished,
+                                  leh);
+  return leh;
+}
+
+
+void
+TALER_AUDITOR_list_exchanges_cancel (
+  struct TALER_AUDITOR_ListExchangesHandle *leh)
+{
+  if (NULL != leh->job)
+  {
+    GNUNET_CURL_job_cancel (leh->job);
+    leh->job = NULL;
+  }
+  GNUNET_free (leh->url);
+  GNUNET_free (leh);
+}
+
+
+/* end of auditor_api_exchanges.c */
diff --git a/src/lib/auditor_api_get_config.c b/src/lib/auditor_api_get_config.c
new file mode 100644
index 00000000..c9f36656
--- /dev/null
+++ b/src/lib/auditor_api_get_config.c
@@ -0,0 +1,288 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2014-2023 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/auditor_api_get_config.c
+ * @brief Implementation of /config for the auditor's HTTP API
+ * @author Sree Harsha Totakura <sreeharsha@totakura.in>
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <microhttpd.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_json_lib.h"
+#include "taler_auditor_service.h"
+#include "taler_signatures.h"
+#include "auditor_api_curl_defaults.h"
+
+
+/**
+ * Which revision of the Taler auditor protocol is implemented
+ * by this library?  Used to determine compatibility.
+ */
+#define TALER_PROTOCOL_CURRENT 0
+
+/**
+ * How many revisions back are we compatible to?
+ */
+#define TALER_PROTOCOL_AGE 0
+
+
+/**
+ * Log error related to CURL operations.
+ *
+ * @param type log level
+ * @param function which function failed to run
+ * @param code what was the curl error code
+ */
+#define CURL_STRERROR(type, function, code)      \
+  GNUNET_log (type, "Curl function `%s' has failed at `%s:%d' with error: %s", 
\
+              function, __FILE__, __LINE__, curl_easy_strerror (code));
+
+
+/**
+ * Handle for the get config request.
+ */
+struct TALER_AUDITOR_GetConfigHandle
+{
+  /**
+   * The context of this handle
+   */
+  struct GNUNET_CURL_Context *ctx;
+
+  /**
+   * Function to call with the auditor's certification data,
+   * NULL if this has already been done.
+   */
+  TALER_AUDITOR_ConfigCallback config_cb;
+
+  /**
+   * Closure to pass to @e config_cb.
+   */
+  void *config_cb_cls;
+
+  /**
+   * Data for the request to get the /config of a auditor,
+   * NULL once we are past stage #MHS_INIT.
+   */
+  struct GNUNET_CURL_Job *vr;
+
+  /**
+   * The url for the @e vr job.
+   */
+  char *vr_url;
+
+};
+
+
+/* ***************** Internal /config fetching ************* */
+
+/**
+ * Decode the JSON in @a resp_obj from the /config response and store the data
+ * in the @a key_data.
+ *
+ * @param[in] resp_obj JSON object to parse
+ * @param[in,out] vi where to store the results we decoded
+ * @param[out] vc where to store config compatibility data
+ * @return #TALER_EC_NONE on success
+ */
+static enum TALER_ErrorCode
+decode_config_json (const json_t *resp_obj,
+                    struct TALER_AUDITOR_ConfigInformation *vi,
+                    enum TALER_AUDITOR_VersionCompatibility *vc)
+{
+  unsigned int age;
+  unsigned int revision;
+  unsigned int current;
+  char dummy;
+  const char *ver;
+  struct GNUNET_JSON_Specification spec[] = {
+    GNUNET_JSON_spec_string ("version",
+                             &ver),
+    GNUNET_JSON_spec_fixed_auto ("auditor_public_key",
+                                 &vi->auditor_pub),
+    GNUNET_JSON_spec_end ()
+  };
+
+  if (JSON_OBJECT != json_typeof (resp_obj))
+  {
+    GNUNET_break_op (0);
+    return TALER_EC_GENERIC_JSON_INVALID;
+  }
+  /* check the config */
+  if (GNUNET_OK !=
+      GNUNET_JSON_parse (resp_obj,
+                         spec,
+                         NULL, NULL))
+  {
+    GNUNET_break_op (0);
+    return TALER_EC_GENERIC_JSON_INVALID;
+  }
+  if (3 != sscanf (ver,
+                   "%u:%u:%u%c",
+                   &current,
+                   &revision,
+                   &age,
+                   &dummy))
+  {
+    GNUNET_break_op (0);
+    return TALER_EC_GENERIC_VERSION_MALFORMED;
+  }
+  vi->version = ver;
+  *vc = TALER_AUDITOR_VC_MATCH;
+  if (TALER_PROTOCOL_CURRENT < current)
+  {
+    *vc |= TALER_AUDITOR_VC_NEWER;
+    if (TALER_PROTOCOL_CURRENT < current - age)
+      *vc |= TALER_AUDITOR_VC_INCOMPATIBLE;
+  }
+  if (TALER_PROTOCOL_CURRENT > current)
+  {
+    *vc |= TALER_AUDITOR_VC_OLDER;
+    if (TALER_PROTOCOL_CURRENT - TALER_PROTOCOL_AGE > current)
+      *vc |= TALER_AUDITOR_VC_INCOMPATIBLE;
+  }
+  return TALER_EC_NONE;
+}
+
+
+/**
+ * Callback used when downloading the reply to a /config request
+ * is complete.
+ *
+ * @param cls the `struct TALER_AUDITOR_GetConfigHandle`
+ * @param response_code HTTP response code, 0 on error
+ * @param gresp_obj parsed JSON result, NULL on error, must be a `const json_t 
*`
+ */
+static void
+config_completed_cb (void *cls,
+                     long response_code,
+                     const void *gresp_obj)
+{
+  struct TALER_AUDITOR_GetConfigHandle *auditor = cls;
+  const json_t *resp_obj = gresp_obj;
+  struct TALER_AUDITOR_ConfigResponse vr = {
+    .hr.reply = resp_obj,
+    .hr.http_status = (unsigned int) response_code
+  };
+
+  auditor->vr = NULL;
+  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+              "Received config from URL `%s' with status %ld.\n",
+              auditor->vr_url,
+              response_code);
+  switch (response_code)
+  {
+  case 0:
+    GNUNET_break_op (0);
+    vr.hr.ec = TALER_EC_INVALID;
+    break;
+  case MHD_HTTP_OK:
+    if (NULL == resp_obj)
+    {
+      GNUNET_break_op (0);
+      vr.hr.http_status = 0;
+      vr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+      break;
+    }
+    vr.hr.ec = decode_config_json (resp_obj,
+                                   &vr.details.ok.vi,
+                                   &vr.details.ok.compat);
+    if (TALER_EC_NONE != vr.hr.ec)
+    {
+      GNUNET_break_op (0);
+      vr.hr.http_status = 0;
+      break;
+    }
+    break;
+  case MHD_HTTP_INTERNAL_SERVER_ERROR:
+    vr.hr.ec = TALER_JSON_get_error_code (resp_obj);
+    vr.hr.hint = TALER_JSON_get_error_hint (resp_obj);
+    break;
+  default:
+    vr.hr.ec = TALER_JSON_get_error_code (resp_obj);
+    vr.hr.hint = TALER_JSON_get_error_hint (resp_obj);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d\n",
+                (unsigned int) response_code,
+                (int) vr.hr.ec);
+    break;
+  }
+  auditor->config_cb (auditor->config_cb_cls,
+                      &vr);
+  TALER_AUDITOR_get_config_cancel (auditor);
+}
+
+
+struct TALER_AUDITOR_GetConfigHandle *
+TALER_AUDITOR_get_config (struct GNUNET_CURL_Context *ctx,
+                          const char *url,
+                          TALER_AUDITOR_ConfigCallback config_cb,
+                          void *config_cb_cls)
+{
+  struct TALER_AUDITOR_GetConfigHandle *auditor;
+  CURL *eh;
+
+  auditor = GNUNET_new (struct TALER_AUDITOR_GetConfigHandle);
+  auditor->config_cb = config_cb;
+  auditor->config_cb_cls = config_cb_cls;
+  auditor->ctx = ctx;
+  auditor->vr_url = TALER_url_join (url,
+                                    "config",
+                                    NULL);
+  if (NULL == auditor->vr_url)
+  {
+    GNUNET_break (0);
+    GNUNET_free (auditor->vr_url);
+    GNUNET_free (auditor);
+    return NULL;
+  }
+  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+              "Requesting auditor config with URL `%s'.\n",
+              auditor->vr_url);
+  eh = TALER_AUDITOR_curl_easy_get_ (auditor->vr_url);
+  if (NULL == eh)
+  {
+    GNUNET_break (0);
+    TALER_AUDITOR_get_config_cancel (auditor);
+    return NULL;
+  }
+  GNUNET_break (CURLE_OK ==
+                curl_easy_setopt (eh,
+                                  CURLOPT_TIMEOUT,
+                                  (long) 300));
+  auditor->vr = GNUNET_CURL_job_add (auditor->ctx,
+                                     eh,
+                                     &config_completed_cb,
+                                     auditor);
+  return auditor;
+}
+
+
+void
+TALER_AUDITOR_get_config_cancel (struct TALER_AUDITOR_GetConfigHandle *auditor)
+{
+  if (NULL != auditor->vr)
+  {
+    GNUNET_CURL_job_cancel (auditor->vr);
+    auditor->vr = NULL;
+  }
+  GNUNET_free (auditor->vr_url);
+  GNUNET_free (auditor);
+}
+
+
+/* end of auditor_api_get_config.c */
diff --git a/src/lib/exchange_api_age_withdraw.c 
b/src/lib/exchange_api_age_withdraw.c
new file mode 100644
index 00000000..ea9c0371
--- /dev/null
+++ b/src/lib/exchange_api_age_withdraw.c
@@ -0,0 +1,1117 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2023 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_age_withdraw.c
+ * @brief Implementation of /reserves/$RESERVE_PUB/age-withdraw requests
+ * @author Özgür Kesim
+ */
+
+#include "platform.h"
+#include <gnunet/gnunet_common.h>
+#include <jansson.h>
+#include <microhttpd.h> /* just for HTTP status codes */
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include <sys/wait.h>
+#include "taler_curl_lib.h"
+#include "taler_error_codes.h"
+#include "taler_json_lib.h"
+#include "taler_exchange_service.h"
+#include "exchange_api_common.h"
+#include "exchange_api_handle.h"
+#include "taler_signatures.h"
+#include "exchange_api_curl_defaults.h"
+#include "taler_util.h"
+
+/**
+ * A CoinCandidate is populated from a master secret
+ */
+struct CoinCandidate
+{
+  /**
+   * Master key material for the coin candidates.
+   */
+  struct TALER_PlanchetMasterSecretP secret;
+
+  /**
+   * The details derived form the master secrets
+   */
+  struct TALER_EXCHANGE_AgeWithdrawCoinPrivateDetails details;
+
+  /**
+   * Blinded hash of the coin
+   **/
+  struct TALER_BlindedCoinHashP blinded_coin_h;
+
+};
+
+
+/**
+ * Closure for a call to /csr-withdraw, contains data that is needed to process
+ * the result.
+ */
+struct CSRClosure
+{
+  /* Points to the actual candidate in CoinData.coin_candidates, to continue
+   * to build its contents based on the results from /csr-withdraw */
+  struct CoinCandidate *candidate;
+
+  /* The planchet to finally generate.  Points to the corresponding candidate
+   * in CoindData.planchet_details */
+  struct TALER_PlanchetDetail *planchet;
+
+  /* Handler to the originating call to /age-withdraw, needed to either
+   * cancel the running age-withdraw request (on failure of the current call
+   * to /csr-withdraw), or to eventually perform the protocol, once all
+   * csr-withdraw requests have successfully finished. */
+  struct TALER_EXCHANGE_AgeWithdrawHandle *age_withdraw_handle;
+
+  /* Denomination information, needed for CS coins for the
+   * step after /csr-withdraw */
+  const struct TALER_EXCHANGE_DenomPublicKey *denom_pub;
+
+  /* Handler for the CS R request */
+  struct TALER_EXCHANGE_CsRWithdrawHandle *csr_withdraw_handle;
+};
+
+/**
+ * Data we keep per coin in the batch.
+ */
+struct CoinData
+{
+  /**
+   * The denomination of the coin.  Must support age restriction, i.e
+   * its .keys.age_mask MUST not be 0
+   */
+  struct TALER_EXCHANGE_DenomPublicKey denom_pub;
+
+  /**
+   * The Candidates for the coin
+   */
+  struct CoinCandidate coin_candidates[TALER_CNC_KAPPA];
+
+  /**
+   * Details of the planchet(s).
+   */
+  struct TALER_PlanchetDetail planchet_details[TALER_CNC_KAPPA];
+
+  /**
+   * Closure for each candidate of type CS for the preflight request to
+   * /csr-withdraw
+   */
+  struct CSRClosure csr_cls[TALER_CNC_KAPPA];
+};
+
+/**
+ * A /reserves/$RESERVE_PUB/age-withdraw request-handle for calls with
+ * pre-blinded planchets.  Returned by TALER_EXCHANGE_age_withdraw_blinded.
+ */
+struct TALER_EXCHANGE_AgeWithdrawBlindedHandle
+{
+
+  /**
+   * Reserve private key.
+   */
+  const struct TALER_ReservePrivateKeyP *reserve_priv;
+
+  /**
+   * Reserve public key, calculated
+   */
+  struct TALER_ReservePublicKeyP reserve_pub;
+
+  /**
+   * Signature of the reserve for the request, calculated after all
+   * parameters for the coins are collected.
+   */
+  struct TALER_ReserveSignatureP reserve_sig;
+
+  /*
+   * The denomination keys of the exchange
+   */
+  struct TALER_EXCHANGE_Keys *keys;
+
+  /**
+   * The age mask, extacted from the denominations.
+   * MUST be the same for all denominations
+   *
+   */
+  struct TALER_AgeMask age_mask;
+
+  /**
+   * Maximum age to commit to.
+   */
+  uint8_t max_age;
+
+  /**
+   * The commitment calculated as SHA512 hash over all blinded_coin_h
+   */
+  struct TALER_AgeWithdrawCommitmentHashP h_commitment;
+
+  /**
+   * Total amount requested (value plus withdraw fee).
+   */
+  struct TALER_Amount amount_with_fee;
+
+  /**
+   * Length of the @e blinded_input Array
+   */
+  size_t num_input;
+
+  /**
+   * The blinded planchet input for the call to /age-withdraw via
+   * TALER_EXCHANGE_age_withdraw_blinded
+   */
+  const struct TALER_EXCHANGE_AgeWithdrawBlindedInput *blinded_input;
+
+  /**
+   * The url for this request.
+   */
+  char *request_url;
+
+  /**
+   * Context for curl.
+   */
+  struct GNUNET_CURL_Context *curl_ctx;
+
+  /**
+   * CURL handle for the request job.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Post Context
+   */
+  struct TALER_CURL_PostContext post_ctx;
+
+  /**
+   * Function to call with age-withdraw response results.
+   */
+  TALER_EXCHANGE_AgeWithdrawBlindedCallback callback;
+
+  /**
+   * Closure for @e blinded_callback
+   */
+  void *callback_cls;
+};
+
+/**
+ * A /reserves/$RESERVE_PUB/age-withdraw request-handle for calls from
+ * a wallet, i. e. when blinding data is available.
+ */
+struct TALER_EXCHANGE_AgeWithdrawHandle
+{
+
+  /**
+   * Length of the @e coin_data Array
+   */
+  size_t num_coins;
+
+  /**
+   * The base-URL of the exchange.
+   */
+  const char *exchange_url;
+
+  /**
+   * Reserve private key.
+   */
+  const struct TALER_ReservePrivateKeyP *reserve_priv;
+
+  /**
+   * Reserve public key, calculated
+   */
+  struct TALER_ReservePublicKeyP reserve_pub;
+
+  /**
+   * Signature of the reserve for the request, calculated after all
+   * parameters for the coins are collected.
+   */
+  struct TALER_ReserveSignatureP reserve_sig;
+
+  /*
+   * The denomination keys of the exchange
+   */
+  struct TALER_EXCHANGE_Keys *keys;
+
+  /**
+   * The age mask, extacted from the denominations.
+   * MUST be the same for all denominations
+   *
+   */
+  struct TALER_AgeMask age_mask;
+
+  /**
+   * Maximum age to commit to.
+   */
+  uint8_t max_age;
+
+  /**
+   * Array of per-coin data
+   */
+  struct CoinData *coin_data;
+
+  /**
+   * Context for curl.
+   */
+  struct GNUNET_CURL_Context *curl_ctx;
+
+  struct
+  {
+    /**
+     * Number of /csr-withdraw requests still pending.
+     */
+    unsigned int pending;
+
+    /**
+     * CURL handle for the request job.
+     */
+    struct GNUNET_CURL_Job *job;
+  } csr;
+
+
+  /**
+   * Function to call with age-withdraw response results.
+   */
+  TALER_EXCHANGE_AgeWithdrawCallback callback;
+
+  /**
+   * Closure for @e age_withdraw_cb
+   */
+  void *callback_cls;
+
+  /* The Handler for the actual call to the exchange */
+  struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *procotol_handle;
+};
+
+/**
+ * We got a 200 OK response for the /reserves/$RESERVE_PUB/age-withdraw 
operation.
+ * Extract the noreveal_index and return it to the caller.
+ *
+ * @param awbh operation handle
+ * @param j_response reply from the exchange
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors
+ */
+static enum GNUNET_GenericReturnValue
+reserve_age_withdraw_ok (
+  struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *awbh,
+  const json_t *j_response)
+{
+  struct TALER_EXCHANGE_AgeWithdrawBlindedResponse response = {
+    .hr.reply = j_response,
+    .hr.http_status = MHD_HTTP_OK,
+    .details.ok.h_commitment = awbh->h_commitment
+  };
+  struct TALER_ExchangeSignatureP exchange_sig;
+  struct GNUNET_JSON_Specification spec[] = {
+    GNUNET_JSON_spec_uint8 ("noreveal_index",
+                            &response.details.ok.noreveal_index),
+    GNUNET_JSON_spec_fixed_auto ("exchange_sig",
+                                 &exchange_sig),
+    GNUNET_JSON_spec_fixed_auto ("exchange_pub",
+                                 &response.details.ok.exchange_pub),
+    GNUNET_JSON_spec_end ()
+  };
+
+  if (GNUNET_OK!=
+      GNUNET_JSON_parse (j_response,
+                         spec,
+                         NULL, NULL))
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+
+  if (GNUNET_OK !=
+      TALER_exchange_online_age_withdraw_confirmation_verify (
+        &awbh->h_commitment,
+        response.details.ok.noreveal_index,
+        &response.details.ok.exchange_pub,
+        &exchange_sig))
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+
+  }
+
+  awbh->callback (awbh->callback_cls,
+                  &response);
+  /* make sure the callback isn't called again */
+  awbh->callback = NULL;
+
+  return GNUNET_OK;
+}
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /reserves/$RESERVE_PUB/age-withdraw request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_AgeWithdrawHandle`
+ * @param response_code The HTTP response code
+ * @param response response data
+ */
+static void
+handle_reserve_age_withdraw_blinded_finished (
+  void *cls,
+  long response_code,
+  const void *response)
+{
+  struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *awbh = cls;
+  const json_t *j_response = response;
+  struct TALER_EXCHANGE_AgeWithdrawBlindedResponse awbr = {
+    .hr.reply = j_response,
+    .hr.http_status = (unsigned int) response_code
+  };
+
+  awbh->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    awbr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    break;
+  case MHD_HTTP_OK:
+    if (GNUNET_OK !=
+        reserve_age_withdraw_ok (awbh,
+                                 j_response))
+    {
+      GNUNET_break_op (0);
+      awbr.hr.http_status = 0;
+      awbr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+      break;
+    }
+    GNUNET_assert (NULL == awbh->callback);
+    TALER_EXCHANGE_age_withdraw_blinded_cancel (awbh);
+    return;
+  case MHD_HTTP_BAD_REQUEST:
+    /* This should never happen, either us or the exchange is buggy
+       (or API version conflict); just pass JSON reply to the application */
+    awbr.hr.ec = TALER_JSON_get_error_code (j_response);
+    awbr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    GNUNET_break_op (0);
+    /* Nothing really to verify, exchange says one of the signatures is
+       invalid; as we checked them, this should never happen, we
+       should pass the JSON reply to the application */
+    awbr.hr.ec = TALER_JSON_get_error_code (j_response);
+    awbr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    /* Nothing really to verify, the exchange basically just says
+       that it doesn't know this reserve.  Can happen if we
+       query before the wire transfer went through.
+       We should simply pass the JSON reply to the application. */
+    awbr.hr.ec = TALER_JSON_get_error_code (j_response);
+    awbr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    break;
+  case MHD_HTTP_CONFLICT:
+    /* The age requirements might not have been met */
+    awbr.hr.ec = TALER_JSON_get_error_code (j_response);
+    awbr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    break;
+  case MHD_HTTP_GONE:
+    /* could happen if denomination was revoked */
+    /* Note: one might want to check /keys for revocation
+       signature here, alas tricky in case our /keys
+       is outdated => left to clients */
+    awbr.hr.ec = TALER_JSON_get_error_code (j_response);
+    awbr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    break;
+  case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS:
+    /* only validate reply is well-formed */
+    {
+      uint64_t ptu;
+      struct GNUNET_JSON_Specification spec[] = {
+        GNUNET_JSON_spec_uint64 ("requirement_row",
+                                 &ptu),
+        GNUNET_JSON_spec_end ()
+      };
+
+      if (GNUNET_OK !=
+          GNUNET_JSON_parse (j_response,
+                             spec,
+                             NULL, NULL))
+      {
+        GNUNET_break_op (0);
+        awbr.hr.http_status = 0;
+        awbr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+        break;
+      }
+    }
+  case MHD_HTTP_INTERNAL_SERVER_ERROR:
+    /* Server had an internal issue; we should retry, but this API
+       leaves this to the application */
+    awbr.hr.ec = TALER_JSON_get_error_code (j_response);
+    awbr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    awbr.hr.ec = TALER_JSON_get_error_code (j_response);
+    awbr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange age-withdraw\n",
+                (unsigned int) response_code,
+                (int) awbr.hr.ec);
+    break;
+  }
+  awbh->callback (awbh->callback_cls,
+                  &awbr);
+  TALER_EXCHANGE_age_withdraw_blinded_cancel (awbh);
+}
+
+
+/**
+ * Runs the actual age-withdraw operation with the blinded planchets.
+ *
+ * @param[in,out] awbh age withdraw handler
+ */
+static void
+perform_protocol (
+  struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *awbh)
+{
+#define FAIL_IF(cond) \
+  do { \
+    if ((cond)) \
+    { \
+      GNUNET_break (! (cond)); \
+      goto ERROR; \
+    } \
+  } while(0)
+
+  struct GNUNET_HashContext *coins_hctx;
+  json_t *j_denoms = NULL;
+  json_t *j_array_candidates = NULL;
+  json_t *j_request_body = NULL;
+  CURL *curlh = NULL;
+
+  GNUNET_assert (0 < awbh->num_input);
+  awbh->age_mask = awbh->blinded_input[0].denom_pub->key.age_mask;
+
+  FAIL_IF (GNUNET_OK !=
+           TALER_amount_set_zero (awbh->keys->currency,
+                                  &awbh->amount_with_fee));
+  /* Accumulate total value with fees */
+  for (size_t i = 0; i < awbh->num_input; i++)
+  {
+    struct TALER_Amount coin_total;
+    const struct TALER_EXCHANGE_DenomPublicKey *dpub =
+      awbh->blinded_input[i].denom_pub;
+
+    FAIL_IF (0 >
+             TALER_amount_add (&coin_total,
+                               &dpub->fees.withdraw,
+                               &dpub->value));
+    FAIL_IF (0 >
+             TALER_amount_add (&awbh->amount_with_fee,
+                               &awbh->amount_with_fee,
+                               &coin_total));
+  }
+
+  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+              "Attempting to age-withdraw from reserve %s with maximum age 
%d\n",
+              TALER_B2S (&awbh->reserve_pub),
+              awbh->max_age);
+
+  coins_hctx = GNUNET_CRYPTO_hash_context_start ();
+  FAIL_IF (NULL == coins_hctx);
+
+
+  j_denoms = json_array ();
+  j_array_candidates = json_array ();
+  FAIL_IF ((NULL == j_denoms) ||
+           (NULL == j_array_candidates));
+
+  for (size_t i  = 0; i< awbh->num_input; i++)
+  {
+    /* Build the denomination array */
+    {
+      const struct TALER_EXCHANGE_DenomPublicKey *denom_pub =
+        awbh->blinded_input[i].denom_pub;
+      const struct TALER_DenominationHashP *denom_h = &denom_pub->h_key;
+      json_t *jdenom;
+
+      /* The mask must be the same for all coins */
+      FAIL_IF (awbh->age_mask.bits != denom_pub->key.age_mask.bits);
+
+      jdenom = GNUNET_JSON_PACK (
+        GNUNET_JSON_pack_data_auto (NULL,
+                                    denom_h));
+      FAIL_IF (NULL == jdenom);
+      FAIL_IF (0 < json_array_append_new (j_denoms,
+                                          jdenom));
+
+      /* Build the candidate array */
+      {
+        json_t *j_can = json_array ();
+        FAIL_IF (NULL == j_can);
+
+        for (size_t k = 0; k < TALER_CNC_KAPPA; k++)
+        {
+          struct TALER_BlindedCoinHashP bch;
+          const struct TALER_PlanchetDetail *planchet =
+            &awbh->blinded_input[i].planchet_details[k];
+          json_t *jc = GNUNET_JSON_PACK (
+            TALER_JSON_pack_blinded_planchet (
+              NULL,
+              &planchet->blinded_planchet));
+
+          FAIL_IF (NULL == jc);
+          FAIL_IF (0 < json_array_append_new (j_can,
+                                              jc));
+
+          TALER_coin_ev_hash (&planchet->blinded_planchet,
+                              &planchet->denom_pub_hash,
+                              &bch);
+
+          GNUNET_CRYPTO_hash_context_read (coins_hctx,
+                                           &bch,
+                                           sizeof(bch));
+        }
+
+        FAIL_IF (0 < json_array_append_new (j_array_candidates,
+                                            j_can));
+      }
+    }
+  }
+
+  /* Build the hash of the commitment */
+  GNUNET_CRYPTO_hash_context_finish (coins_hctx,
+                                     &awbh->h_commitment.hash);
+
+  /* Sign the request */
+  TALER_wallet_age_withdraw_sign (&awbh->h_commitment,
+                                  &awbh->amount_with_fee,
+                                  &awbh->age_mask,
+                                  awbh->max_age,
+                                  awbh->reserve_priv,
+                                  &awbh->reserve_sig);
+
+  /* Initiate the POST-request */
+  j_request_body = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_array_steal ("denom_hs", j_denoms),
+    GNUNET_JSON_pack_array_steal ("blinded_coin_evs", j_array_candidates),
+    GNUNET_JSON_pack_uint64 ("max_age", awbh->max_age),
+    GNUNET_JSON_pack_data_auto ("reserve_sig", &awbh->reserve_sig));
+  FAIL_IF (NULL == j_request_body);
+
+  curlh = TALER_EXCHANGE_curl_easy_get_ (awbh->request_url);
+  FAIL_IF (NULL == curlh);
+  FAIL_IF (GNUNET_OK !=
+           TALER_curl_easy_post (&awbh->post_ctx,
+                                 curlh,
+                                 j_request_body));
+  json_decref (j_request_body);
+  j_request_body = NULL;
+
+  awbh->job = GNUNET_CURL_job_add2 (
+    awbh->curl_ctx,
+    curlh,
+    awbh->post_ctx.headers,
+    &handle_reserve_age_withdraw_blinded_finished,
+    awbh);
+  FAIL_IF (NULL == awbh->job);
+
+  /* No errors, return */
+  return;
+
+ERROR:
+  if (NULL != j_denoms)
+    json_decref (j_denoms);
+  if (NULL != j_array_candidates)
+    json_decref (j_array_candidates);
+  if (NULL != j_request_body)
+    json_decref (j_request_body);
+  if (NULL != curlh)
+    curl_easy_cleanup (curlh);
+  TALER_EXCHANGE_age_withdraw_blinded_cancel (awbh);
+  return;
+#undef FAIL_IF
+}
+
+
+/**
+ * @brief Callback to copy the results from the call to 
TALER_age_withdraw_blinded
+ * to the result for the originating call from TALER_age_withdraw.
+ *
+ * @param cls struct TALER_AgeWithdrawHandle
+ * @param awbr The response
+ */
+static void
+copy_results (
+  void *cls,
+  const struct TALER_EXCHANGE_AgeWithdrawBlindedResponse *awbr)
+{
+  struct TALER_EXCHANGE_AgeWithdrawHandle *awh = cls;
+  uint8_t k =  awbr->details.ok.noreveal_index;
+  struct TALER_EXCHANGE_AgeWithdrawCoinPrivateDetails details[awh->num_coins];
+  struct TALER_BlindedCoinHashP blinded_coin_hs[awh->num_coins];
+  struct TALER_EXCHANGE_AgeWithdrawResponse resp = {
+    .hr = awbr->hr,
+    .details = {
+      .ok = { .noreveal_index = awbr->details.ok.noreveal_index,
+              .h_commitment = awbr->details.ok.h_commitment,
+              .exchange_pub = awbr->details.ok.exchange_pub,
+              .num_coins = awh->num_coins,
+              .coin_details = details,
+              .blinded_coin_hs = blinded_coin_hs},
+    },
+  };
+
+  for (size_t n = 0; n< awh->num_coins; n++)
+  {
+    details[n] = awh->coin_data[n].coin_candidates[k].details;
+    details[n].planchet = awh->coin_data[n].planchet_details[k];
+    blinded_coin_hs[n] = awh->coin_data[n].coin_candidates[k].blinded_coin_h;
+  }
+
+  awh->callback (awh->callback_cls,
+                 &resp);
+
+  awh->callback = NULL;
+}
+
+
+/**
+ * @brief Prepares and executes TALER_EXCHANGE_age_withdraw_blinded.
+ * If there were CS-denominations involved, started once the all calls
+ * to /csr-withdraw are done.
+ */
+static void
+call_age_withdraw_blinded (
+  struct TALER_EXCHANGE_AgeWithdrawHandle *awh)
+{
+  struct TALER_EXCHANGE_AgeWithdrawBlindedInput blinded_input[awh->num_coins];
+
+  /* Prepare the blinded planchets as input */
+  for (size_t n = 0; n < awh->num_coins; n++)
+  {
+    blinded_input[n].denom_pub = &awh->coin_data[n].denom_pub;
+    for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++)
+      blinded_input[n].planchet_details[k] =
+        awh->coin_data[n].planchet_details[k];
+  }
+
+  awh->procotol_handle =
+    TALER_EXCHANGE_age_withdraw_blinded (
+      awh->curl_ctx,
+      awh->keys,
+      awh->exchange_url,
+      awh->reserve_priv,
+      awh->max_age,
+      awh->num_coins,
+      blinded_input,
+      copy_results,
+      awh);
+}
+
+
+/**
+ * Prepares the request URL for the age-withdraw request
+ *
+ * @param awbh The handler
+ * @param exchange_url The base-URL to the exchange
+ */
+static
+enum GNUNET_GenericReturnValue
+prepare_url (
+  struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *awbh,
+  const char *exchange_url)
+{
+  char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 32];
+  char pub_str[sizeof (struct TALER_ReservePublicKeyP) * 2];
+  char *end;
+
+  end = GNUNET_STRINGS_data_to_string (
+    &awbh->reserve_pub,
+    sizeof (awbh->reserve_pub),
+    pub_str,
+    sizeof (pub_str));
+  *end = '\0';
+  GNUNET_snprintf (arg_str,
+                   sizeof (arg_str),
+                   "reserves/%s/age-withdraw",
+                   pub_str);
+
+  awbh->request_url = TALER_url_join (exchange_url,
+                                      arg_str,
+                                      NULL);
+  if (NULL == awbh->request_url)
+  {
+    GNUNET_break (0);
+    TALER_EXCHANGE_age_withdraw_blinded_cancel (awbh);
+    return GNUNET_SYSERR;
+  }
+
+  return GNUNET_OK;
+}
+
+
+/**
+ * @brief Function called when CSR withdraw retrieval is finished
+ *
+ * @param cls the `struct CSRClosure *`
+ * @param csrr replies from the /csr-withdraw request
+ */
+static void
+csr_withdraw_done (
+  void *cls,
+  const struct TALER_EXCHANGE_CsRWithdrawResponse *csrr)
+{
+  struct CSRClosure *csr = cls;
+  struct CoinCandidate *can;
+  struct TALER_PlanchetDetail *planchet;
+  struct TALER_EXCHANGE_AgeWithdrawHandle *awh;
+
+  GNUNET_assert (NULL != csr);
+  awh = csr->age_withdraw_handle;
+  planchet = csr->planchet;
+  can = csr->candidate;
+
+  GNUNET_assert (NULL != can);
+  GNUNET_assert (NULL != planchet);
+  GNUNET_assert (NULL != awh);
+
+  csr->csr_withdraw_handle = NULL;
+
+  switch (csrr->hr.http_status)
+  {
+  case MHD_HTTP_OK:
+    {
+      bool success = false;
+      /* Complete the initialization of the coin with CS denomination */
+      can->details.alg_values = csrr->details.ok.alg_values;
+      GNUNET_assert (can->details.alg_values.cipher
+                     == TALER_DENOMINATION_CS);
+      TALER_planchet_setup_coin_priv (&can->secret,
+                                      &can->details.alg_values,
+                                      &can->details.coin_priv);
+      TALER_planchet_blinding_secret_create (&can->secret,
+                                             &can->details.alg_values,
+                                             &can->details.blinding_key);
+      /* This initializes the 2nd half of the
+         can->planchet_detail.blinded_planchet! */
+      do {
+        if (GNUNET_OK !=
+            TALER_planchet_prepare (&csr->denom_pub->key,
+                                    &can->details.alg_values,
+                                    &can->details.blinding_key,
+                                    &can->details.coin_priv,
+                                    &can->details.h_age_commitment,
+                                    &can->details.h_coin_pub,
+                                    planchet))
+        {
+          GNUNET_break (0);
+          TALER_EXCHANGE_age_withdraw_cancel (awh);
+          break;
+        }
+
+        if (GNUNET_OK !=
+            TALER_coin_ev_hash (&planchet->blinded_planchet,
+                                &planchet->denom_pub_hash,
+                                &can->blinded_coin_h))
+        {
+          GNUNET_break (0);
+          TALER_EXCHANGE_age_withdraw_cancel (awh);
+          break;
+        }
+        success = true;
+      } while(0);
+
+      awh->csr.pending--;
+
+      /* No more pending requests to /csr-withdraw, we can now perform the
+       * actual age-withdraw operation */
+      if (0 == awh->csr.pending && success)
+        call_age_withdraw_blinded (awh);
+      return;
+    }
+  default:
+    break;
+  }
+
+  TALER_EXCHANGE_age_withdraw_cancel (awh);
+}
+
+
+/**
+ * @brief Prepare the coins for the call to age-withdraw and calculates
+ * the total amount with fees.
+ *
+ * For denomination with CS as cipher, initiates the preflight to retrieve the
+ * csr-parameter via /csr-withdraw.
+ *
+ * @param awh The handler to the age-withdraw
+ * @param num_coins The number of coins in @e coin_inputs
+ * @param coin_inputs The input for the individual coin(-candidates)
+ * @return GNUNET_OK on success, GNUNET_SYSERR on failure
+ */
+static
+enum GNUNET_GenericReturnValue
+prepare_coins (
+  struct TALER_EXCHANGE_AgeWithdrawHandle *awh,
+  size_t num_coins,
+  const struct TALER_EXCHANGE_AgeWithdrawCoinInput coin_inputs[
+    static num_coins])
+{
+#define FAIL_IF(cond) \
+  do { \
+    if ((cond)) \
+    { \
+      GNUNET_break (! (cond)); \
+      goto ERROR; \
+    } \
+  } while(0)
+
+  GNUNET_assert (0 < num_coins);
+  awh->age_mask = coin_inputs[0].denom_pub->key.age_mask;
+
+  awh->coin_data = GNUNET_new_array (awh->num_coins,
+                                     struct CoinData);
+
+  for (size_t i = 0; i < num_coins; i++)
+  {
+    struct CoinData *cd = &awh->coin_data[i];
+    const struct TALER_EXCHANGE_AgeWithdrawCoinInput *input = &coin_inputs[i];
+    cd->denom_pub = *input->denom_pub;
+
+    /* The mask must be the same for all coins */
+    FAIL_IF (awh->age_mask.bits != input->denom_pub->key.age_mask.bits);
+
+    TALER_denom_pub_deep_copy (&cd->denom_pub.key,
+                               &input->denom_pub->key);
+
+    for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++)
+    {
+      struct CoinCandidate *can = &cd->coin_candidates[k];
+      struct TALER_PlanchetDetail *planchet = &cd->planchet_details[k];
+
+      can->secret = input->secrets[k];
+      /* Derive the age restriction from the given secret and
+       * the maximum age */
+      TALER_age_restriction_from_secret (
+        &can->secret,
+        &input->denom_pub->key.age_mask,
+        awh->max_age,
+        &can->details.age_commitment_proof);
+
+      TALER_age_commitment_hash (&can->details.age_commitment_proof.commitment,
+                                 &can->details.h_age_commitment);
+
+      switch (input->denom_pub->key.cipher)
+      {
+      case TALER_DENOMINATION_RSA:
+        {
+          can->details.alg_values.cipher = TALER_DENOMINATION_RSA;
+          TALER_planchet_setup_coin_priv (&can->secret,
+                                          &can->details.alg_values,
+                                          &can->details.coin_priv);
+          TALER_planchet_blinding_secret_create (&can->secret,
+                                                 &can->details.alg_values,
+                                                 &can->details.blinding_key);
+          FAIL_IF (GNUNET_OK !=
+                   TALER_planchet_prepare (&cd->denom_pub.key,
+                                           &can->details.alg_values,
+                                           &can->details.blinding_key,
+                                           &can->details.coin_priv,
+                                           &can->details.h_age_commitment,
+                                           &can->details.h_coin_pub,
+                                           planchet));
+          FAIL_IF (GNUNET_OK !=
+                   TALER_coin_ev_hash (&planchet->blinded_planchet,
+                                       &planchet->denom_pub_hash,
+                                       &can->blinded_coin_h));
+          break;
+        }
+      case TALER_DENOMINATION_CS:
+        {
+          can->details.alg_values.cipher = TALER_DENOMINATION_CS;
+
+          struct CSRClosure *cls = &cd->csr_cls[k];
+          /**
+           * Save the handler and the denomination for the callback
+           * after the call to csr-withdraw */
+          cls->age_withdraw_handle = awh;
+          cls->candidate = can;
+          cls->planchet = planchet;
+          cls->denom_pub = &cd->denom_pub;
+
+          TALER_cs_withdraw_nonce_derive (
+            &can->secret,
+            &planchet->blinded_planchet.details.cs_blinded_planchet.nonce);
+
+          /* Note that we only initialize the first half
+             of the blinded_planchet here; the other part
+             will be done after the /csr-withdraw request! */
+          planchet->blinded_planchet.cipher = TALER_DENOMINATION_CS;
+          cls->csr_withdraw_handle =
+            TALER_EXCHANGE_csr_withdraw (
+              awh->curl_ctx,
+              awh->exchange_url,
+              &cd->denom_pub,
+              &planchet->blinded_planchet.details.cs_blinded_planchet.nonce,
+              &csr_withdraw_done,
+              cls);
+          FAIL_IF (NULL == cls->csr_withdraw_handle);
+
+          awh->csr.pending++;
+          break;
+        }
+      default:
+        FAIL_IF (1);
+      }
+    }
+  }
+  return GNUNET_OK;
+
+ERROR:
+  TALER_EXCHANGE_age_withdraw_cancel (awh);
+  return GNUNET_SYSERR;
+#undef FAIL_IF
+};
+
+struct TALER_EXCHANGE_AgeWithdrawHandle *
+TALER_EXCHANGE_age_withdraw (
+  struct GNUNET_CURL_Context *curl_ctx,
+  struct TALER_EXCHANGE_Keys *keys,
+  const char *exchange_url,
+  const struct TALER_ReservePrivateKeyP *reserve_priv,
+  size_t num_coins,
+  const struct TALER_EXCHANGE_AgeWithdrawCoinInput coin_inputs[const static
+                                                               num_coins],
+  uint8_t max_age,
+  TALER_EXCHANGE_AgeWithdrawCallback res_cb,
+  void *res_cb_cls)
+{
+  struct TALER_EXCHANGE_AgeWithdrawHandle *awh;
+
+  awh = GNUNET_new (struct TALER_EXCHANGE_AgeWithdrawHandle);
+  awh->exchange_url = exchange_url;
+  awh->keys = TALER_EXCHANGE_keys_incref (keys);
+  awh->curl_ctx = curl_ctx;
+  awh->reserve_priv = reserve_priv;
+  awh->callback = res_cb;
+  awh->callback_cls = res_cb_cls;
+  awh->num_coins = num_coins;
+  awh->max_age = max_age;
+
+
+  if (GNUNET_OK != prepare_coins (awh,
+                                  num_coins,
+                                  coin_inputs))
+    return NULL;
+
+  /* If there were no CS denominations, we can now perform the actual
+   * age-withdraw protocol.  Otherwise, there are calls to /csr-withdraw
+   * in flight and once they finish, the age-withdraw-protocol will be
+   * called from within the csr_withdraw_done-function.
+   */
+  if (0 == awh->csr.pending)
+    call_age_withdraw_blinded (awh);
+
+  return awh;
+}
+
+
+void
+TALER_EXCHANGE_age_withdraw_cancel (
+  struct TALER_EXCHANGE_AgeWithdrawHandle *awh)
+{
+  /* Cleanup coin data */
+  for (unsigned int i = 0; i<awh->num_coins; i++)
+  {
+    struct CoinData *cd = &awh->coin_data[i];
+
+    for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++)
+    {
+      struct TALER_PlanchetDetail *planchet = &cd->planchet_details[k];
+      struct CSRClosure *cls = &cd->csr_cls[k];
+
+      if (NULL != cls->csr_withdraw_handle)
+      {
+        TALER_EXCHANGE_csr_withdraw_cancel (cls->csr_withdraw_handle);
+        cls->csr_withdraw_handle = NULL;
+      }
+      TALER_blinded_planchet_free (&planchet->blinded_planchet);
+    }
+    TALER_denom_pub_free (&cd->denom_pub.key);
+  }
+  GNUNET_free (awh->coin_data);
+  TALER_EXCHANGE_keys_decref (awh->keys);
+  TALER_EXCHANGE_age_withdraw_blinded_cancel (awh->procotol_handle);
+  awh->procotol_handle = NULL;
+  GNUNET_free (awh);
+}
+
+
+struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *
+TALER_EXCHANGE_age_withdraw_blinded (
+  struct GNUNET_CURL_Context *curl_ctx,
+  struct TALER_EXCHANGE_Keys *keys,
+  const char *exchange_url,
+  const struct TALER_ReservePrivateKeyP *reserve_priv,
+  uint8_t max_age,
+  unsigned int num_input,
+  const struct TALER_EXCHANGE_AgeWithdrawBlindedInput blinded_input[static
+                                                                    num_input],
+  TALER_EXCHANGE_AgeWithdrawBlindedCallback res_cb,
+  void *res_cb_cls)
+{
+  struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *awbh =
+    GNUNET_new (struct TALER_EXCHANGE_AgeWithdrawBlindedHandle);
+
+  awbh->num_input = num_input;
+  awbh->blinded_input = blinded_input;
+  awbh->keys = TALER_EXCHANGE_keys_incref (keys);
+  awbh->curl_ctx = curl_ctx;
+  awbh->reserve_priv = reserve_priv;
+  awbh->callback = res_cb;
+  awbh->callback_cls = res_cb_cls;
+  awbh->max_age = max_age;
+
+  GNUNET_CRYPTO_eddsa_key_get_public (&awbh->reserve_priv->eddsa_priv,
+                                      &awbh->reserve_pub.eddsa_pub);
+
+  if (GNUNET_OK != prepare_url (awbh,
+                                exchange_url))
+    return NULL;
+
+  perform_protocol (awbh);
+  return awbh;
+}
+
+
+void
+TALER_EXCHANGE_age_withdraw_blinded_cancel (
+  struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *awbh)
+{
+  if (NULL == awbh)
+    return;
+
+  if (NULL != awbh->job)
+  {
+    GNUNET_CURL_job_cancel (awbh->job);
+    awbh->job = NULL;
+  }
+  GNUNET_free (awbh->request_url);
+  TALER_EXCHANGE_keys_decref (awbh->keys);
+  TALER_curl_easy_post_finished (&awbh->post_ctx);
+  GNUNET_free (awbh);
+}
+
+
+/* exchange_api_age_withdraw.c */
diff --git a/src/lib/exchange_api_age_withdraw_reveal.c 
b/src/lib/exchange_api_age_withdraw_reveal.c
new file mode 100644
index 00000000..a448d109
--- /dev/null
+++ b/src/lib/exchange_api_age_withdraw_reveal.c
@@ -0,0 +1,471 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2023 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_age_withdraw_reveal.c
+ * @brief Implementation of /age-withdraw/$ACH/reveal requests
+ * @author Özgür Kesim
+ */
+
+#include "platform.h"
+#include <gnunet/gnunet_common.h>
+#include <jansson.h>
+#include <microhttpd.h> /* just for HTTP status codes */
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_curl_lib.h"
+#include "taler_json_lib.h"
+#include "taler_exchange_service.h"
+#include "exchange_api_common.h"
+#include "exchange_api_handle.h"
+#include "taler_signatures.h"
+#include "exchange_api_curl_defaults.h"
+
+/**
+ * Handler for a running age-withdraw-reveal  request
+ */
+struct TALER_EXCHANGE_AgeWithdrawRevealHandle
+{
+
+  /* The index not to be disclosed */
+  uint8_t noreveal_index;
+
+  /* The age-withdraw commitment */
+  struct TALER_AgeWithdrawCommitmentHashP h_commitment;
+
+  /* The reserve's public key */
+  const struct TALER_ReservePublicKeyP *reserve_pub;
+
+  /* Number of coins */
+  size_t num_coins;
+
+  /* The @e num_coins * kappa coin secrets from the age-withdraw commitment */
+  const struct TALER_EXCHANGE_AgeWithdrawCoinInput *coins_input;
+
+  /* The url for the reveal request */
+  const char *request_url;
+
+  /**
+   * CURL handle for the request job.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Post Context
+   */
+  struct TALER_CURL_PostContext post_ctx;
+
+  /* Callback */
+  TALER_EXCHANGE_AgeWithdrawRevealCallback callback;
+
+  /* Reveal */
+  void *callback_cls;
+};
+
+
+/**
+ * We got a 200 OK response for the /age-withdraw/$ACH/reveal operation.
+ * Extract the signed blindedcoins and return it to the caller.
+ *
+ * @param awrh operation handle
+ * @param j_response reply from the exchange
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors
+ */
+static enum GNUNET_GenericReturnValue
+age_withdraw_reveal_ok (
+  struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh,
+  const json_t *j_response)
+{
+  struct TALER_EXCHANGE_AgeWithdrawRevealResponse response = {
+    .hr.reply = j_response,
+    .hr.http_status = MHD_HTTP_OK,
+  };
+  const json_t *j_sigs;
+  struct GNUNET_JSON_Specification spec[] = {
+    GNUNET_JSON_spec_array_const ("ev_sigs",
+                                  &j_sigs),
+    GNUNET_JSON_spec_end ()
+  };
+
+  if (GNUNET_OK != GNUNET_JSON_parse (j_response,
+                                      spec,
+                                      NULL, NULL))
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+
+  if (awrh->num_coins != json_array_size (j_sigs))
+  {
+    /* Number of coins generated does not match our expectation */
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+
+  {
+    struct TALER_BlindedDenominationSignature denom_sigs[awrh->num_coins];
+    json_t *j_sig;
+    size_t n;
+
+    /* Reconstruct the coins and unblind the signatures */
+    json_array_foreach (j_sigs, n, j_sig)
+    {
+      struct GNUNET_JSON_Specification spec[] = {
+        TALER_JSON_spec_blinded_denom_sig (NULL,
+                                           &denom_sigs[n]),
+        GNUNET_JSON_spec_end ()
+      };
+
+      if (GNUNET_OK != GNUNET_JSON_parse (j_sig,
+                                          spec,
+                                          NULL, NULL))
+      {
+        GNUNET_break_op (0);
+        return GNUNET_SYSERR;
+      }
+
+    }
+
+    response.details.ok.num_sigs = awrh->num_coins;
+    response.details.ok.blinded_denom_sigs = denom_sigs;
+    awrh->callback (awrh->callback_cls,
+                    &response);
+    /* Make sure the callback isn't called again */
+    awrh->callback = NULL;
+  }
+
+  return GNUNET_OK;
+}
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /age-withdraw/$ACH/reveal request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_AgeWithdrawRevealHandle`
+ * @param response_code The HTTP response code
+ * @param response response data
+ */
+static void
+handle_age_withdraw_reveal_finished (
+  void *cls,
+  long response_code,
+  const void *response)
+{
+  struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh = cls;
+  const json_t *j_response = response;
+  struct TALER_EXCHANGE_AgeWithdrawRevealResponse awr = {
+    .hr.reply = j_response,
+    .hr.http_status = (unsigned int) response_code
+  };
+
+  awrh->job = NULL;
+  /* FIXME[oec]: Only handle response-codes that are in the spec */
+  switch (response_code)
+  {
+  case 0:
+    awr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    break;
+  case MHD_HTTP_OK:
+    {
+      enum GNUNET_GenericReturnValue ret;
+
+      ret = age_withdraw_reveal_ok (awrh,
+                                    j_response);
+      if (GNUNET_OK != ret)
+      {
+        GNUNET_break_op (0);
+        awr.hr.http_status = 0;
+        awr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+        break;
+      }
+      GNUNET_assert (NULL == awrh->callback);
+      TALER_EXCHANGE_age_withdraw_reveal_cancel (awrh);
+      return;
+    }
+  case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS:
+    /* only validate reply is well-formed */
+    {
+      uint64_t ptu;
+      struct GNUNET_JSON_Specification spec[] = {
+        GNUNET_JSON_spec_uint64 ("legitimization_uuid",
+                                 &ptu),
+        GNUNET_JSON_spec_end ()
+      };
+
+      if (GNUNET_OK !=
+          GNUNET_JSON_parse (j_response,
+                             spec,
+                             NULL, NULL))
+      {
+        GNUNET_break_op (0);
+        awr.hr.http_status = 0;
+        awr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+        break;
+      }
+    }
+  case MHD_HTTP_BAD_REQUEST:
+    /* This should never happen, either us or the exchange is buggy
+       (or API version conflict); just pass JSON reply to the application */
+    awr.hr.ec = TALER_JSON_get_error_code (j_response);
+    awr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    GNUNET_break_op (0);
+    /**
+     * This should never happen, as we don't sent any signatures
+     * to the exchange to verify.  We should simply pass the JSON reply
+     * to the application
+     **/
+    awr.hr.ec = TALER_JSON_get_error_code (j_response);
+    awr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    /* Nothing really to verify, the exchange basically just says
+       that it doesn't know this age-withdraw commitment. */
+    awr.hr.ec = TALER_JSON_get_error_code (j_response);
+    awr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    break;
+  case MHD_HTTP_CONFLICT:
+    /* An age commitment for one of the coins did not fulfill
+     * the required maximum age requirement of the corresponding
+     * reserve.
+     * Error code: TALER_EC_EXCHANGE_GENERIC_COIN_AGE_REQUIREMENT_FAILURE.
+     */
+    awr.hr.ec = TALER_JSON_get_error_code (j_response);
+    awr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    break;
+  case MHD_HTTP_GONE:
+    /* could happen if denomination was revoked */
+    /* Note: one might want to check /keys for revocation
+       signature here, alas tricky in case our /keys
+       is outdated => left to clients */
+    awr.hr.ec = TALER_JSON_get_error_code (j_response);
+    awr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    break;
+  case MHD_HTTP_INTERNAL_SERVER_ERROR:
+    /* Server had an internal issue; we should retry, but this API
+       leaves this to the application */
+    awr.hr.ec = TALER_JSON_get_error_code (j_response);
+    awr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    awr.hr.ec = TALER_JSON_get_error_code (j_response);
+    awr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange age-withdraw\n",
+                (unsigned int) response_code,
+                (int) awr.hr.ec);
+    break;
+  }
+  awrh->callback (awrh->callback_cls,
+                  &awr);
+  TALER_EXCHANGE_age_withdraw_reveal_cancel (awrh);
+}
+
+
+/**
+ * Prepares the request URL for the age-withdraw-reveal request
+ *
+ * @param exchange_url The base-URL to the exchange
+ * @param[in,out] awrh The handler
+ * @return GNUNET_OK on success, GNUNET_SYSERR otherwise
+ */
+static
+enum GNUNET_GenericReturnValue
+prepare_url (
+  const char *exchange_url,
+  struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh)
+{
+  char arg_str[sizeof (struct TALER_AgeWithdrawCommitmentHashP) * 2 + 32];
+  char pub_str[sizeof (struct TALER_AgeWithdrawCommitmentHashP) * 2];
+  char *end;
+
+  end = GNUNET_STRINGS_data_to_string (&awrh->h_commitment,
+                                       sizeof (awrh->h_commitment),
+                                       pub_str,
+                                       sizeof (pub_str));
+  *end = '\0';
+  GNUNET_snprintf (arg_str,
+                   sizeof (arg_str),
+                   "age-withdraw/%s/reveal",
+                   pub_str);
+
+  awrh->request_url = TALER_url_join (exchange_url,
+                                      arg_str,
+                                      NULL);
+  if (NULL == awrh->request_url)
+  {
+    GNUNET_break (0);
+    TALER_EXCHANGE_age_withdraw_reveal_cancel (awrh);
+    return GNUNET_SYSERR;
+  }
+
+  return GNUNET_OK;
+}
+
+
+/**
+ * Call /age-withdraw/$ACH/reveal
+ *
+ * @param curl_ctx The context for CURL
+ * @param awrh The handler
+ */
+static
+void
+perform_protocol (
+  struct GNUNET_CURL_Context *curl_ctx,
+  struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh)
+{
+  CURL *curlh = NULL;
+  json_t *j_request_body = NULL;
+  json_t *j_array_of_secrets = NULL;
+  json_t *j_secrets = NULL;
+  json_t *j_sec = NULL;
+
+#define FAIL_IF(cond) \
+  do { \
+    if ((cond)) \
+    { \
+      GNUNET_break (! (cond)); \
+      goto ERROR; \
+    } \
+  } while(0)
+
+  j_array_of_secrets = json_array ();
+  FAIL_IF (NULL == j_array_of_secrets);
+
+  for (size_t n = 0; n < awrh->num_coins; n++)
+  {
+    const struct TALER_PlanchetMasterSecretP *secrets =
+      awrh->coins_input[n].secrets;
+
+    j_secrets = json_array ();
+    FAIL_IF (NULL == j_secrets);
+
+    for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++)
+    {
+      const struct TALER_PlanchetMasterSecretP *secret = &secrets[k];
+      if (awrh->noreveal_index == k)
+        continue;
+
+      j_sec = GNUNET_JSON_PACK (
+        GNUNET_JSON_pack_data_auto (NULL, secret));
+
+      FAIL_IF (NULL == j_sec);
+      FAIL_IF (0 < json_array_append_new (j_secrets,
+                                          j_sec));
+    }
+
+    FAIL_IF (0 < json_array_append_new (j_array_of_secrets,
+                                        j_secrets));
+  }
+  j_request_body = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_data_auto ("reserve_pub",
+                                awrh->reserve_pub),
+    GNUNET_JSON_pack_array_steal ("disclosed_coin_secrets",
+                                  j_array_of_secrets));
+  FAIL_IF (NULL == j_request_body);
+
+  curlh = TALER_EXCHANGE_curl_easy_get_ (awrh->request_url);
+  FAIL_IF (NULL == curlh);
+  FAIL_IF (GNUNET_OK !=
+           TALER_curl_easy_post (&awrh->post_ctx,
+                                 curlh,
+                                 j_request_body));
+  json_decref (j_request_body);
+  j_request_body = NULL;
+
+  awrh->job = GNUNET_CURL_job_add2 (curl_ctx,
+                                    curlh,
+                                    awrh->post_ctx.headers,
+                                    &handle_age_withdraw_reveal_finished,
+                                    awrh);
+  FAIL_IF (NULL == awrh->job);
+
+  /* No error, return */
+  return;
+
+ERROR:
+  if (NULL != j_sec)
+    json_decref (j_sec);
+  if (NULL != j_secrets)
+    json_decref (j_secrets);
+  if (NULL != j_array_of_secrets)
+    json_decref (j_array_of_secrets);
+  if (NULL != j_request_body)
+    json_decref (j_request_body);
+  if (NULL != curlh)
+    curl_easy_cleanup (curlh);
+  TALER_EXCHANGE_age_withdraw_reveal_cancel (awrh);
+  return;
+#undef FAIL_IF
+}
+
+
+struct TALER_EXCHANGE_AgeWithdrawRevealHandle *
+TALER_EXCHANGE_age_withdraw_reveal (
+  struct GNUNET_CURL_Context *curl_ctx,
+  const char *exchange_url,
+  size_t num_coins,
+  const struct TALER_EXCHANGE_AgeWithdrawCoinInput coins_input[static
+                                                               num_coins],
+  uint8_t noreveal_index,
+  const struct TALER_AgeWithdrawCommitmentHashP *h_commitment,
+  const struct TALER_ReservePublicKeyP *reserve_pub,
+  TALER_EXCHANGE_AgeWithdrawRevealCallback reveal_cb,
+  void *reveal_cb_cls)
+{
+  struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh =
+    GNUNET_new (struct TALER_EXCHANGE_AgeWithdrawRevealHandle);
+  awrh->noreveal_index = noreveal_index;
+  awrh->h_commitment = *h_commitment;
+  awrh->num_coins = num_coins;
+  awrh->coins_input = coins_input;
+  awrh->callback = reveal_cb;
+  awrh->callback_cls = reveal_cb_cls;
+  awrh->reserve_pub = reserve_pub;
+
+  if (GNUNET_OK !=
+      prepare_url (exchange_url,
+                   awrh))
+    return NULL;
+
+  perform_protocol (curl_ctx, awrh);
+
+  return awrh;
+}
+
+
+void
+TALER_EXCHANGE_age_withdraw_reveal_cancel (
+  struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh)
+{
+  if (NULL != awrh->job)
+  {
+    GNUNET_CURL_job_cancel (awrh->job);
+    awrh->job = NULL;
+  }
+  TALER_curl_easy_post_finished (&awrh->post_ctx);
+  /* FIXME[oec]: anything else left to cleanup!? */
+  GNUNET_free (awrh);
+}
+
+
+/* exchange_api_age_withdraw_reveal.c */
diff --git a/src/lib/exchange_api_auditor_add_denomination.c 
b/src/lib/exchange_api_auditor_add_denomination.c
new file mode 100644
index 00000000..89de0d7f
--- /dev/null
+++ b/src/lib/exchange_api_auditor_add_denomination.c
@@ -0,0 +1,238 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2015-2021 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_auditor_add_denomination.c
+ * @brief functions for the auditor to add its signature for denomination at 
the exchange
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "taler_json_lib.h"
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_exchange_service.h"
+#include "auditor_api_curl_defaults.h"
+#include "taler_signatures.h"
+#include "taler_curl_lib.h"
+#include "taler_json_lib.h"
+
+
+struct TALER_EXCHANGE_AuditorAddDenominationHandle
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Minor context that holds body and headers.
+   */
+  struct TALER_CURL_PostContext post_ctx;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_AuditorAddDenominationCallback cb;
+
+  /**
+   * Closure for @a cb.
+   */
+  void *cb_cls;
+
+  /**
+   * Reference to the execution context.
+   */
+  struct GNUNET_CURL_Context *ctx;
+};
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP POST /auditor/$AUDITOR_PUB/$H_DENOM_PUB request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_AuditorAddDenominationHandle *`
+ * @param response_code HTTP response code, 0 on error
+ * @param response response body, NULL if not in JSON
+ */
+static void
+handle_auditor_add_denomination_finished (void *cls,
+                                          long response_code,
+                                          const void *response)
+{
+  struct TALER_EXCHANGE_AuditorAddDenominationHandle *ah = cls;
+  const json_t *json = response;
+  struct TALER_EXCHANGE_AuditorAddDenominationResponse adr = {
+    .hr.http_status = (unsigned int) response_code,
+    .hr.reply = json
+  };
+
+  ah->job = NULL;
+  switch (response_code)
+  {
+  case MHD_HTTP_NO_CONTENT:
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    adr.hr.ec = TALER_JSON_get_error_code (json);
+    adr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    adr.hr.ec = TALER_JSON_get_error_code (json);
+    adr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_GONE:
+    adr.hr.ec = TALER_JSON_get_error_code (json);
+    adr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_PRECONDITION_FAILED:
+    adr.hr.ec = TALER_JSON_get_error_code (json);
+    adr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  default:
+    /* unexpected response code */
+    if (NULL != json)
+    {
+      adr.hr.ec = TALER_JSON_get_error_code (json);
+      adr.hr.hint = TALER_JSON_get_error_hint (json);
+      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                  "Unexpected response code %u/%d for exchange 
auditor-add-denomination at URL `%s'\n",
+                  (unsigned int) response_code,
+                  (int) adr.hr.ec,
+                  ah->url);
+    }
+    else
+    {
+      adr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+      adr.hr.hint = NULL;
+      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                  "Unexpected HTTP response code %u (no JSON returned) at URL 
`%s'\n",
+                  (unsigned int) response_code,
+                  ah->url);
+    }
+    break;
+  }
+  if (NULL != ah->cb)
+  {
+    ah->cb (ah->cb_cls,
+            &adr);
+    ah->cb = NULL;
+  }
+  TALER_EXCHANGE_add_auditor_denomination_cancel (ah);
+}
+
+
+struct TALER_EXCHANGE_AuditorAddDenominationHandle *
+TALER_EXCHANGE_add_auditor_denomination (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  const struct TALER_DenominationHashP *h_denom_pub,
+  const struct TALER_AuditorPublicKeyP *auditor_pub,
+  const struct TALER_AuditorSignatureP *auditor_sig,
+  TALER_EXCHANGE_AuditorAddDenominationCallback cb,
+  void *cb_cls)
+{
+  struct TALER_EXCHANGE_AuditorAddDenominationHandle *ah;
+  CURL *eh;
+  json_t *body;
+
+  ah = GNUNET_new (struct TALER_EXCHANGE_AuditorAddDenominationHandle);
+  ah->cb = cb;
+  ah->cb_cls = cb_cls;
+  ah->ctx = ctx;
+  {
+    char apub_str[sizeof (*auditor_pub) * 2];
+    char denom_str[sizeof (*h_denom_pub) * 2];
+    char arg_str[sizeof (apub_str) + sizeof (denom_str) + 32];
+    char *end;
+
+    end = GNUNET_STRINGS_data_to_string (auditor_pub,
+                                         sizeof (*auditor_pub),
+                                         apub_str,
+                                         sizeof (apub_str));
+    *end = '\0';
+    end = GNUNET_STRINGS_data_to_string (h_denom_pub,
+                                         sizeof (*h_denom_pub),
+                                         denom_str,
+                                         sizeof (denom_str));
+    *end = '\0';
+    GNUNET_snprintf (arg_str,
+                     sizeof (arg_str),
+                     "auditors/%s/%s",
+                     apub_str,
+                     denom_str);
+    ah->url = TALER_url_join (url,
+                              arg_str,
+                              NULL);
+  }
+  if (NULL == ah->url)
+  {
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Could not construct request URL.\n");
+    GNUNET_free (ah);
+    return NULL;
+  }
+  body = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_data_auto ("auditor_sig",
+                                auditor_sig));
+  eh = TALER_AUDITOR_curl_easy_get_ (ah->url);
+  if ( (NULL == eh) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&ah->post_ctx,
+                              eh,
+                              body)) )
+  {
+    GNUNET_break (0);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    json_decref (body);
+    GNUNET_free (ah->url);
+    return NULL;
+  }
+  json_decref (body);
+  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+              "Requesting URL '%s'\n",
+              ah->url);
+  ah->job = GNUNET_CURL_job_add2 (ctx,
+                                  eh,
+                                  ah->post_ctx.headers,
+                                  &handle_auditor_add_denomination_finished,
+                                  ah);
+  if (NULL == ah->job)
+  {
+    TALER_EXCHANGE_add_auditor_denomination_cancel (ah);
+    return NULL;
+  }
+  return ah;
+}
+
+
+void
+TALER_EXCHANGE_add_auditor_denomination_cancel (
+  struct TALER_EXCHANGE_AuditorAddDenominationHandle *ah)
+{
+  if (NULL != ah->job)
+  {
+    GNUNET_CURL_job_cancel (ah->job);
+    ah->job = NULL;
+  }
+  TALER_curl_easy_post_finished (&ah->post_ctx);
+  GNUNET_free (ah->url);
+  GNUNET_free (ah);
+}
diff --git a/src/lib/exchange_api_csr_melt.c b/src/lib/exchange_api_csr_melt.c
new file mode 100644
index 00000000..f59995af
--- /dev/null
+++ b/src/lib/exchange_api_csr_melt.c
@@ -0,0 +1,317 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2014-2022 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_csr_melt.c
+ * @brief Implementation of /csr-melt requests (get R in exchange used for 
Clause Schnorr refresh)
+ * @author Lucien Heuzeveldt
+ * @author Gian Demarmels
+ */
+#include "platform.h"
+#include <jansson.h>
+#include <microhttpd.h> /* just for HTTP status codes */
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_exchange_service.h"
+#include "taler_json_lib.h"
+#include "exchange_api_handle.h"
+#include "taler_signatures.h"
+#include "exchange_api_curl_defaults.h"
+
+
+/**
+ * @brief A Clause Schnorr R Handle
+ */
+struct TALER_EXCHANGE_CsRMeltHandle
+{
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_CsRMeltCallback cb;
+
+  /**
+   * Closure for @a cb.
+   */
+  void *cb_cls;
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Context for #TEH_curl_easy_post(). Keeps the data that must
+   * persist for Curl to make the upload.
+   */
+  struct TALER_CURL_PostContext post_ctx;
+};
+
+
+/**
+ * We got a 200 OK response for the /reserves/$RESERVE_PUB/withdraw operation.
+ * Extract the coin's signature and return it to the caller.  The signature we
+ * get from the exchange is for the blinded value.  Thus, we first must
+ * unblind it and then should verify its validity against our coin's hash.
+ *
+ * If everything checks out, we return the unblinded signature
+ * to the application via the callback.
+ *
+ * @param csrh operation handle
+ * @param arr reply from the exchange
+ * @param hr http response details
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors
+ */
+static enum GNUNET_GenericReturnValue
+csr_ok (struct TALER_EXCHANGE_CsRMeltHandle *csrh,
+        const json_t *arr,
+        struct TALER_EXCHANGE_HttpResponse *hr)
+{
+  unsigned int alen = json_array_size (arr);
+  struct TALER_ExchangeWithdrawValues alg_values[GNUNET_NZL (alen)];
+  struct TALER_EXCHANGE_CsRMeltResponse csrr = {
+    .hr = *hr,
+    .details.ok.alg_values_len = alen,
+    .details.ok.alg_values = alg_values
+  };
+
+  for (unsigned int i = 0; i<alen; i++)
+  {
+    json_t *av = json_array_get (arr,
+                                 i);
+    struct GNUNET_JSON_Specification spec[] = {
+      TALER_JSON_spec_exchange_withdraw_values (
+        "ewv",
+        &alg_values[i]),
+      GNUNET_JSON_spec_end ()
+    };
+
+    if (GNUNET_OK !=
+        GNUNET_JSON_parse (av,
+                           spec,
+                           NULL, NULL))
+    {
+      GNUNET_break_op (0);
+      return GNUNET_SYSERR;
+    }
+  }
+  csrh->cb (csrh->cb_cls,
+            &csrr);
+  return GNUNET_OK;
+}
+
+
+/**
+ * Function called when we're done processing the HTTP /csr request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_CsRMeltHandle`
+ * @param response_code HTTP response code, 0 on error
+ * @param response parsed JSON result, NULL on error
+ */
+static void
+handle_csr_finished (void *cls,
+                     long response_code,
+                     const void *response)
+{
+  struct TALER_EXCHANGE_CsRMeltHandle *csrh = cls;
+  const json_t *j = response;
+  struct TALER_EXCHANGE_HttpResponse hr = {
+    .reply = j,
+    .http_status = (unsigned int) response_code
+  };
+  struct TALER_EXCHANGE_CsRMeltResponse csrr = {
+    .hr = hr
+  };
+
+  csrh->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    csrr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    break;
+  case MHD_HTTP_OK:
+    {
+      json_t *arr;
+
+      arr = json_object_get (j,
+                             "ewvs");
+      if ( (NULL == arr) ||
+           (0 == json_array_size (arr)) ||
+           (GNUNET_OK !=
+            csr_ok (csrh,
+                    arr,
+                    &hr)) )
+      {
+        GNUNET_break_op (0);
+        csrr.hr.http_status = 0;
+        csrr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+        break;
+      }
+    }
+    TALER_EXCHANGE_csr_melt_cancel (csrh);
+    return;
+  case MHD_HTTP_BAD_REQUEST:
+    /* This should never happen, either us or the exchange is buggy
+       (or API version conflict); just pass JSON reply to the application */
+    csrr.hr.ec = TALER_JSON_get_error_code (j);
+    csrr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    /* Nothing really to verify, the exchange basically just says
+       that it doesn't know the /csr endpoint or denomination.
+       Can happen if the exchange doesn't support Clause Schnorr.
+       We should simply pass the JSON reply to the application. */
+    csrr.hr.ec = TALER_JSON_get_error_code (j);
+    csrr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_GONE:
+    /* could happen if denomination was revoked */
+    /* Note: one might want to check /keys for revocation
+       signature here, alas tricky in case our /keys
+       is outdated => left to clients */
+    csrr.hr.ec = TALER_JSON_get_error_code (j);
+    csrr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_INTERNAL_SERVER_ERROR:
+    /* Server had an internal issue; we should retry, but this API
+       leaves this to the application */
+    csrr.hr.ec = TALER_JSON_get_error_code (j);
+    csrr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    csrr.hr.ec = TALER_JSON_get_error_code (j);
+    csrr.hr.hint = TALER_JSON_get_error_hint (j);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for CS R request\n",
+                (unsigned int) response_code,
+                (int) hr.ec);
+    break;
+  }
+  csrh->cb (csrh->cb_cls,
+            &csrr);
+  csrh->cb = NULL;
+  TALER_EXCHANGE_csr_melt_cancel (csrh);
+}
+
+
+struct TALER_EXCHANGE_CsRMeltHandle *
+TALER_EXCHANGE_csr_melt (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  const struct TALER_RefreshMasterSecretP *rms,
+  unsigned int nks_len,
+  struct TALER_EXCHANGE_NonceKey nks[static nks_len],
+  TALER_EXCHANGE_CsRMeltCallback res_cb,
+  void *res_cb_cls)
+{
+  struct TALER_EXCHANGE_CsRMeltHandle *csrh;
+  json_t *csr_arr;
+
+  if (0 == nks_len)
+  {
+    GNUNET_break (0);
+    return NULL;
+  }
+  for (unsigned int i = 0; i<nks_len; i++)
+    if (TALER_DENOMINATION_CS != nks[i].pk->key.cipher)
+    {
+      GNUNET_break (0);
+      return NULL;
+    }
+  csrh = GNUNET_new (struct TALER_EXCHANGE_CsRMeltHandle);
+  csrh->cb = res_cb;
+  csrh->cb_cls = res_cb_cls;
+  csr_arr = json_array ();
+  GNUNET_assert (NULL != csr_arr);
+  for (unsigned int i = 0; i<nks_len; i++)
+  {
+    const struct TALER_EXCHANGE_NonceKey *nk = &nks[i];
+    json_t *csr_obj;
+
+    csr_obj = GNUNET_JSON_PACK (
+      GNUNET_JSON_pack_uint64 ("coin_offset",
+                               nk->cnc_num),
+      GNUNET_JSON_pack_data_auto ("denom_pub_hash",
+                                  &nk->pk->h_key));
+    GNUNET_assert (NULL != csr_obj);
+    GNUNET_assert (0 ==
+                   json_array_append_new (csr_arr,
+                                          csr_obj));
+  }
+  csrh->url = TALER_url_join (url,
+                              "csr-melt",
+                              NULL);
+  if (NULL == csrh->url)
+  {
+    json_decref (csr_arr);
+    GNUNET_free (csrh);
+    return NULL;
+  }
+  {
+    CURL *eh;
+    json_t *req;
+
+    req = GNUNET_JSON_PACK (
+      GNUNET_JSON_pack_data_auto ("rms",
+                                  rms),
+      GNUNET_JSON_pack_array_steal ("nks",
+                                    csr_arr));
+    eh = TALER_EXCHANGE_curl_easy_get_ (csrh->url);
+    if ( (NULL == eh) ||
+         (GNUNET_OK !=
+          TALER_curl_easy_post (&csrh->post_ctx,
+                                eh,
+                                req)) )
+    {
+      GNUNET_break (0);
+      if (NULL != eh)
+        curl_easy_cleanup (eh);
+      json_decref (req);
+      GNUNET_free (csrh->url);
+      GNUNET_free (csrh);
+      return NULL;
+    }
+    json_decref (req);
+    csrh->job = GNUNET_CURL_job_add2 (ctx,
+                                      eh,
+                                      csrh->post_ctx.headers,
+                                      &handle_csr_finished,
+                                      csrh);
+  }
+  return csrh;
+}
+
+
+void
+TALER_EXCHANGE_csr_melt_cancel (struct TALER_EXCHANGE_CsRMeltHandle *csrh)
+{
+  if (NULL != csrh->job)
+  {
+    GNUNET_CURL_job_cancel (csrh->job);
+    csrh->job = NULL;
+  }
+  GNUNET_free (csrh->url);
+  TALER_curl_easy_post_finished (&csrh->post_ctx);
+  GNUNET_free (csrh);
+}
diff --git a/src/lib/exchange_api_kyc_check.c b/src/lib/exchange_api_kyc_check.c
new file mode 100644
index 00000000..373dd89a
--- /dev/null
+++ b/src/lib/exchange_api_kyc_check.c
@@ -0,0 +1,329 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2021-2023 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_kyc_check.c
+ * @brief Implementation of the /kyc-check request
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <microhttpd.h> /* just for HTTP check codes */
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_exchange_service.h"
+#include "taler_json_lib.h"
+#include "exchange_api_handle.h"
+#include "taler_signatures.h"
+#include "exchange_api_curl_defaults.h"
+
+
+/**
+ * @brief A ``/kyc-check`` handle
+ */
+struct TALER_EXCHANGE_KycCheckHandle
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Keys of the exchange.
+   */
+  struct TALER_EXCHANGE_Keys *keys;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_KycStatusCallback cb;
+
+  /**
+   * Closure for @e cb.
+   */
+  void *cb_cls;
+
+  /**
+   * Hash of the payto:// URL that is being KYC'ed.
+   */
+  struct TALER_PaytoHashP h_payto;
+
+};
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /kyc-check request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_KycCheckHandle`
+ * @param response_code HTTP response code, 0 on error
+ * @param response parsed JSON result, NULL on error
+ */
+static void
+handle_kyc_check_finished (void *cls,
+                           long response_code,
+                           const void *response)
+{
+  struct TALER_EXCHANGE_KycCheckHandle *kch = cls;
+  const json_t *j = response;
+  struct TALER_EXCHANGE_KycStatus ks = {
+    .http_status = (unsigned int) response_code
+  };
+
+  kch->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    ks.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    break;
+  case MHD_HTTP_OK:
+    {
+      const json_t *kyc_details;
+      uint32_t status;
+      struct GNUNET_JSON_Specification spec[] = {
+        GNUNET_JSON_spec_fixed_auto ("exchange_sig",
+                                     &ks.details.ok.exchange_sig),
+        GNUNET_JSON_spec_fixed_auto ("exchange_pub",
+                                     &ks.details.ok.exchange_pub),
+        GNUNET_JSON_spec_timestamp ("now",
+                                    &ks.details.ok.timestamp),
+        GNUNET_JSON_spec_object_const ("kyc_details",
+                                       &kyc_details),
+        GNUNET_JSON_spec_uint32 ("aml_status",
+                                 &status),
+        GNUNET_JSON_spec_end ()
+      };
+
+      if (GNUNET_OK !=
+          GNUNET_JSON_parse (j,
+                             spec,
+                             NULL, NULL))
+      {
+        GNUNET_break_op (0);
+        ks.http_status = 0;
+        ks.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+        break;
+      }
+      ks.details.ok.kyc_details = kyc_details;
+      ks.details.ok.aml_status
+        = (enum TALER_AmlDecisionState) status;
+      if (GNUNET_OK !=
+          TALER_EXCHANGE_test_signing_key (kch->keys,
+                                           &ks.details.ok.exchange_pub))
+      {
+        GNUNET_break_op (0);
+        ks.http_status = 0;
+        ks.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+        GNUNET_JSON_parse_free (spec);
+        break;
+      }
+
+      if (GNUNET_OK !=
+          TALER_exchange_online_account_setup_success_verify (
+            &kch->h_payto,
+            ks.details.ok.kyc_details,
+            ks.details.ok.timestamp,
+            &ks.details.ok.exchange_pub,
+            &ks.details.ok.exchange_sig))
+      {
+        GNUNET_break_op (0);
+        ks.http_status = 0;
+        ks.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+        GNUNET_JSON_parse_free (spec);
+        break;
+      }
+      kch->cb (kch->cb_cls,
+               &ks);
+      GNUNET_JSON_parse_free (spec);
+      TALER_EXCHANGE_kyc_check_cancel (kch);
+      return;
+    }
+  case MHD_HTTP_ACCEPTED:
+    {
+      uint32_t status;
+      struct GNUNET_JSON_Specification spec[] = {
+        GNUNET_JSON_spec_string ("kyc_url",
+                                 &ks.details.accepted.kyc_url),
+        GNUNET_JSON_spec_uint32 ("aml_status",
+                                 &status),
+        GNUNET_JSON_spec_end ()
+      };
+
+      if (GNUNET_OK !=
+          GNUNET_JSON_parse (j,
+                             spec,
+                             NULL, NULL))
+      {
+        GNUNET_break_op (0);
+        ks.http_status = 0;
+        ks.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+        break;
+      }
+      ks.details.accepted.aml_status
+        = (enum TALER_AmlDecisionState) status;
+      kch->cb (kch->cb_cls,
+               &ks);
+      GNUNET_JSON_parse_free (spec);
+      TALER_EXCHANGE_kyc_check_cancel (kch);
+      return;
+    }
+  case MHD_HTTP_NO_CONTENT:
+    break;
+  case MHD_HTTP_BAD_REQUEST:
+    ks.ec = TALER_JSON_get_error_code (j);
+    /* This should never happen, either us or the exchange is buggy
+       (or API version conflict); just pass JSON reply to the application */
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    ks.ec = TALER_JSON_get_error_code (j);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    ks.ec = TALER_JSON_get_error_code (j);
+    break;
+  case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS:
+    {
+      uint32_t status;
+      struct GNUNET_JSON_Specification spec[] = {
+        GNUNET_JSON_spec_uint32 ("aml_status",
+                                 &status),
+        GNUNET_JSON_spec_end ()
+      };
+
+      if (GNUNET_OK !=
+          GNUNET_JSON_parse (j,
+                             spec,
+                             NULL, NULL))
+      {
+        GNUNET_break_op (0);
+        ks.http_status = 0;
+        ks.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+        break;
+      }
+      ks.details.unavailable_for_legal_reasons.aml_status
+        = (enum TALER_AmlDecisionState) status;
+      kch->cb (kch->cb_cls,
+               &ks);
+      GNUNET_JSON_parse_free (spec);
+      TALER_EXCHANGE_kyc_check_cancel (kch);
+      return;
+    }
+  case MHD_HTTP_INTERNAL_SERVER_ERROR:
+    ks.ec = TALER_JSON_get_error_code (j);
+    /* Server had an internal issue; we should retry, but this API
+       leaves this to the application */
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    ks.ec = TALER_JSON_get_error_code (j);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange kyc_check\n",
+                (unsigned int) response_code,
+                (int) ks.ec);
+    break;
+  }
+  kch->cb (kch->cb_cls,
+           &ks);
+  TALER_EXCHANGE_kyc_check_cancel (kch);
+}
+
+
+struct TALER_EXCHANGE_KycCheckHandle *
+TALER_EXCHANGE_kyc_check (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  struct TALER_EXCHANGE_Keys *keys,
+  uint64_t requirement_row,
+  const struct TALER_PaytoHashP *h_payto,
+  enum TALER_KYCLOGIC_KycUserType ut,
+  struct GNUNET_TIME_Relative timeout,
+  TALER_EXCHANGE_KycStatusCallback cb,
+  void *cb_cls)
+{
+  struct TALER_EXCHANGE_KycCheckHandle *kch;
+  CURL *eh;
+  char *arg_str;
+
+  {
+    char payto_str[sizeof (*h_payto) * 2];
+    char *end;
+    unsigned long long timeout_ms;
+
+    end = GNUNET_STRINGS_data_to_string (
+      h_payto,
+      sizeof (*h_payto),
+      payto_str,
+      sizeof (payto_str) - 1);
+    *end = '\0';
+    timeout_ms = timeout.rel_value_us
+                 / GNUNET_TIME_UNIT_MILLISECONDS.rel_value_us;
+    GNUNET_asprintf (&arg_str,
+                     "kyc-check/%llu/%s/%s?timeout_ms=%llu",
+                     (unsigned long long) requirement_row,
+                     payto_str,
+                     TALER_KYCLOGIC_kyc_user_type2s (ut),
+                     timeout_ms);
+  }
+  kch = GNUNET_new (struct TALER_EXCHANGE_KycCheckHandle);
+  kch->h_payto = *h_payto;
+  kch->cb = cb;
+  kch->cb_cls = cb_cls;
+  kch->url = TALER_url_join (url,
+                             arg_str,
+                             NULL);
+  GNUNET_free (arg_str);
+  if (NULL == kch->url)
+  {
+    GNUNET_free (kch);
+    return NULL;
+  }
+  eh = TALER_EXCHANGE_curl_easy_get_ (kch->url);
+  if (NULL == eh)
+  {
+    GNUNET_break (0);
+    GNUNET_free (kch->url);
+    GNUNET_free (kch);
+    return NULL;
+  }
+  kch->keys = TALER_EXCHANGE_keys_incref (keys);
+  kch->job = GNUNET_CURL_job_add_with_ct_json (ctx,
+                                               eh,
+                                               &handle_kyc_check_finished,
+                                               kch);
+  return kch;
+}
+
+
+void
+TALER_EXCHANGE_kyc_check_cancel (struct TALER_EXCHANGE_KycCheckHandle *kch)
+{
+  if (NULL != kch->job)
+  {
+    GNUNET_CURL_job_cancel (kch->job);
+    kch->job = NULL;
+  }
+  TALER_EXCHANGE_keys_decref (kch->keys);
+  GNUNET_free (kch->url);
+  GNUNET_free (kch);
+}
+
+
+/* end of exchange_api_kyc_check.c */
diff --git a/src/lib/exchange_api_kyc_proof.c b/src/lib/exchange_api_kyc_proof.c
new file mode 100644
index 00000000..e7cc9c4c
--- /dev/null
+++ b/src/lib/exchange_api_kyc_proof.c
@@ -0,0 +1,217 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2021, 2022 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_kyc_proof.c
+ * @brief Implementation of the /kyc-proof request
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <microhttpd.h> /* just for HTTP proof codes */
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_exchange_service.h"
+#include "taler_json_lib.h"
+#include "exchange_api_handle.h"
+#include "taler_signatures.h"
+#include "exchange_api_curl_defaults.h"
+
+
+/**
+ * @brief A ``/kyc-proof`` handle
+ */
+struct TALER_EXCHANGE_KycProofHandle
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Handle to our CURL request.
+   */
+  CURL *eh;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_KycProofCallback cb;
+
+  /**
+   * Closure for @e cb.
+   */
+  void *cb_cls;
+
+};
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /kyc-proof request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_KycProofHandle`
+ * @param response_code HTTP response code, 0 on error
+ * @param body response body
+ * @param body_size number of bytes in @a body
+ */
+static void
+handle_kyc_proof_finished (void *cls,
+                           long response_code,
+                           const void *body,
+                           size_t body_size)
+{
+  struct TALER_EXCHANGE_KycProofHandle *kph = cls;
+  struct TALER_EXCHANGE_KycProofResponse kpr = {
+    .http_status = (unsigned int) response_code
+  };
+
+  (void) body;
+  (void) body_size;
+  kph->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    break;
+  case MHD_HTTP_SEE_OTHER:
+    {
+      char *redirect_url;
+
+      GNUNET_assert (CURLE_OK ==
+                     curl_easy_getinfo (kph->eh,
+                                        CURLINFO_REDIRECT_URL,
+                                        &redirect_url));
+      kpr.details.found.redirect_url = redirect_url;
+      break;
+    }
+  case MHD_HTTP_BAD_REQUEST:
+    /* This should never happen, either us or the exchange is buggy
+       (or API version conflict); just pass JSON reply to the application */
+    break;
+  case MHD_HTTP_UNAUTHORIZED:
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    break;
+  case MHD_HTTP_BAD_GATEWAY:
+    /* Server had an internal issue; we should retry, but this API
+       leaves this to the application */
+    break;
+  case MHD_HTTP_GATEWAY_TIMEOUT:
+    /* Server had an internal issue; we should retry, but this API
+       leaves this to the application */
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u for exchange kyc_proof\n",
+                (unsigned int) response_code);
+    break;
+  }
+  kph->cb (kph->cb_cls,
+           &kpr);
+  TALER_EXCHANGE_kyc_proof_cancel (kph);
+}
+
+
+struct TALER_EXCHANGE_KycProofHandle *
+TALER_EXCHANGE_kyc_proof (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  const struct TALER_PaytoHashP *h_payto,
+  const char *logic,
+  const char *args,
+  TALER_EXCHANGE_KycProofCallback cb,
+  void *cb_cls)
+{
+  struct TALER_EXCHANGE_KycProofHandle *kph;
+  char *arg_str;
+
+  if (NULL == args)
+    args = "";
+  else
+    GNUNET_assert (args[0] == '&');
+  {
+    char hstr[sizeof (struct TALER_PaytoHashP) * 2];
+    char *end;
+
+    end = GNUNET_STRINGS_data_to_string (h_payto,
+                                         sizeof (*h_payto),
+                                         hstr,
+                                         sizeof (hstr));
+    *end = '\0';
+    GNUNET_asprintf (&arg_str,
+                     "kyc-proof/%s?state=%s%s",
+                     logic,
+                     hstr,
+                     args);
+  }
+  kph = GNUNET_new (struct TALER_EXCHANGE_KycProofHandle);
+  kph->cb = cb;
+  kph->cb_cls = cb_cls;
+  kph->url = TALER_url_join (url,
+                             arg_str,
+                             NULL);
+  GNUNET_free (arg_str);
+  if (NULL == kph->url)
+  {
+    GNUNET_free (kph);
+    return NULL;
+  }
+  kph->eh = TALER_EXCHANGE_curl_easy_get_ (kph->url);
+  if (NULL == kph->eh)
+  {
+    GNUNET_break (0);
+    GNUNET_free (kph->url);
+    GNUNET_free (kph);
+    return NULL;
+  }
+  /* disable location following, we want to learn the
+     result of a 303 redirect! */
+  GNUNET_assert (CURLE_OK ==
+                 curl_easy_setopt (kph->eh,
+                                   CURLOPT_FOLLOWLOCATION,
+                                   0L));
+  kph->job = GNUNET_CURL_job_add_raw (ctx,
+                                      kph->eh,
+                                      NULL,
+                                      &handle_kyc_proof_finished,
+                                      kph);
+  return kph;
+}
+
+
+void
+TALER_EXCHANGE_kyc_proof_cancel (struct TALER_EXCHANGE_KycProofHandle *kph)
+{
+  if (NULL != kph->job)
+  {
+    GNUNET_CURL_job_cancel (kph->job);
+    kph->job = NULL;
+  }
+  GNUNET_free (kph->url);
+  GNUNET_free (kph);
+}
+
+
+/* end of exchange_api_kyc_proof.c */
diff --git a/src/lib/exchange_api_kyc_wallet.c 
b/src/lib/exchange_api_kyc_wallet.c
new file mode 100644
index 00000000..7197694a
--- /dev/null
+++ b/src/lib/exchange_api_kyc_wallet.c
@@ -0,0 +1,230 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2021 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_kyc_wallet.c
+ * @brief Implementation of the /kyc-wallet request
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <microhttpd.h> /* just for HTTP wallet codes */
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_exchange_service.h"
+#include "taler_json_lib.h"
+#include "exchange_api_handle.h"
+#include "taler_signatures.h"
+#include "exchange_api_curl_defaults.h"
+
+
+/**
+ * @brief A ``/kyc-wallet`` handle
+ */
+struct TALER_EXCHANGE_KycWalletHandle
+{
+
+  /**
+   * Context for #TEH_curl_easy_post(). Keeps the data that must
+   * persist for Curl to make the upload.
+   */
+  struct TALER_CURL_PostContext ctx;
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_KycWalletCallback cb;
+
+  /**
+   * Closure for @e cb.
+   */
+  void *cb_cls;
+
+};
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /kyc-wallet request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_KycWalletHandle`
+ * @param response_code HTTP response code, 0 on error
+ * @param response parsed JSON result, NULL on error
+ */
+static void
+handle_kyc_wallet_finished (void *cls,
+                            long response_code,
+                            const void *response)
+{
+  struct TALER_EXCHANGE_KycWalletHandle *kwh = cls;
+  const json_t *j = response;
+  struct TALER_EXCHANGE_WalletKycResponse ks = {
+    .http_status = (unsigned int) response_code
+  };
+
+  kwh->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    ks.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    break;
+  case MHD_HTTP_NO_CONTENT:
+    break;
+  case MHD_HTTP_BAD_REQUEST:
+    ks.ec = TALER_JSON_get_error_code (j);
+    /* This should never happen, either us or the exchange is buggy
+       (or API version conflict); just pass JSON reply to the application */
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    ks.ec = TALER_JSON_get_error_code (j);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    ks.ec = TALER_JSON_get_error_code (j);
+    break;
+  case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS:
+    {
+      struct GNUNET_JSON_Specification spec[] = {
+        GNUNET_JSON_spec_fixed_auto (
+          "h_payto",
+          &ks.details.unavailable_for_legal_reasons.h_payto),
+        GNUNET_JSON_spec_uint64 (
+          "requirement_row",
+          &ks.details.unavailable_for_legal_reasons.requirement_row),
+        GNUNET_JSON_spec_end ()
+      };
+
+      if (GNUNET_OK !=
+          GNUNET_JSON_parse (j,
+                             spec,
+                             NULL, NULL))
+      {
+        GNUNET_break_op (0);
+        ks.http_status = 0;
+        ks.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+        break;
+      }
+      break;
+    }
+  case MHD_HTTP_INTERNAL_SERVER_ERROR:
+    ks.ec = TALER_JSON_get_error_code (j);
+    /* Server had an internal issue; we should retry, but this API
+       leaves this to the application */
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    ks.ec = TALER_JSON_get_error_code (j);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange /kyc-wallet\n",
+                (unsigned int) response_code,
+                (int) ks.ec);
+    break;
+  }
+  kwh->cb (kwh->cb_cls,
+           &ks);
+  TALER_EXCHANGE_kyc_wallet_cancel (kwh);
+}
+
+
+struct TALER_EXCHANGE_KycWalletHandle *
+TALER_EXCHANGE_kyc_wallet (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  const struct TALER_ReservePrivateKeyP *reserve_priv,
+  const struct TALER_Amount *balance,
+  TALER_EXCHANGE_KycWalletCallback cb,
+  void *cb_cls)
+{
+  struct TALER_EXCHANGE_KycWalletHandle *kwh;
+  CURL *eh;
+  json_t *req;
+  struct TALER_ReservePublicKeyP reserve_pub;
+  struct TALER_ReserveSignatureP reserve_sig;
+
+  GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv,
+                                      &reserve_pub.eddsa_pub);
+  TALER_wallet_account_setup_sign (reserve_priv,
+                                   balance,
+                                   &reserve_sig);
+  req = GNUNET_JSON_PACK (
+    TALER_JSON_pack_amount ("balance",
+                            balance),
+    GNUNET_JSON_pack_data_auto ("reserve_pub",
+                                &reserve_pub),
+    GNUNET_JSON_pack_data_auto ("reserve_sig",
+                                &reserve_sig));
+  GNUNET_assert (NULL != req);
+  kwh = GNUNET_new (struct TALER_EXCHANGE_KycWalletHandle);
+  kwh->cb = cb;
+  kwh->cb_cls = cb_cls;
+  kwh->url = TALER_url_join (url,
+                             "kyc-wallet",
+                             NULL);
+  if (NULL == kwh->url)
+  {
+    json_decref (req);
+    GNUNET_free (kwh);
+    return NULL;
+  }
+  eh = TALER_EXCHANGE_curl_easy_get_ (kwh->url);
+  if ( (NULL == eh) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&kwh->ctx,
+                              eh,
+                              req)) )
+  {
+    GNUNET_break (0);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    json_decref (req);
+    GNUNET_free (kwh->url);
+    GNUNET_free (kwh);
+    return NULL;
+  }
+  json_decref (req);
+  kwh->job = GNUNET_CURL_job_add2 (ctx,
+                                   eh,
+                                   kwh->ctx.headers,
+                                   &handle_kyc_wallet_finished,
+                                   kwh);
+  return kwh;
+}
+
+
+void
+TALER_EXCHANGE_kyc_wallet_cancel (struct TALER_EXCHANGE_KycWalletHandle *kwh)
+{
+  if (NULL != kwh->job)
+  {
+    GNUNET_CURL_job_cancel (kwh->job);
+    kwh->job = NULL;
+  }
+  GNUNET_free (kwh->url);
+  TALER_curl_easy_post_finished (&kwh->ctx);
+  GNUNET_free (kwh);
+}
+
+
+/* end of exchange_api_kyc_wallet.c */
diff --git a/src/lib/exchange_api_lookup_aml_decision.c 
b/src/lib/exchange_api_lookup_aml_decision.c
new file mode 100644
index 00000000..01e98213
--- /dev/null
+++ b/src/lib/exchange_api_lookup_aml_decision.c
@@ -0,0 +1,419 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2023 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_lookup_aml_decision.c
+ * @brief Implementation of the /aml/$OFFICER_PUB/decision request
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <microhttpd.h> /* just for HTTP status codes */
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_exchange_service.h"
+#include "taler_json_lib.h"
+#include "exchange_api_handle.h"
+#include "taler_signatures.h"
+#include "exchange_api_curl_defaults.h"
+
+
+/**
+ * @brief A /coins/$COIN_PUB/link Handle
+ */
+struct TALER_EXCHANGE_LookupAmlDecision
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_LookupAmlDecisionCallback decision_cb;
+
+  /**
+   * Closure for @e cb.
+   */
+  void *decision_cb_cls;
+
+  /**
+   * HTTP headers for the job.
+   */
+  struct curl_slist *job_headers;
+};
+
+
+/**
+ * Parse AML decision history.
+ *
+ * @param aml_history JSON array with AML history
+ * @param[out] aml_history_ar where to write the result
+ * @return #GNUNET_OK on success
+ */
+static enum GNUNET_GenericReturnValue
+parse_aml_history (const json_t *aml_history,
+                   struct TALER_EXCHANGE_AmlDecisionDetail *aml_history_ar)
+{
+  json_t *obj;
+  size_t idx;
+
+  json_array_foreach (aml_history, idx, obj)
+  {
+    struct TALER_EXCHANGE_AmlDecisionDetail *aml = &aml_history_ar[idx];
+    uint32_t state32;
+    struct GNUNET_JSON_Specification spec[] = {
+      GNUNET_JSON_spec_timestamp ("decision_time",
+                                  &aml->decision_time),
+      GNUNET_JSON_spec_string ("justification",
+                               &aml->justification),
+      TALER_JSON_spec_amount_any ("new_threshold",
+                                  &aml->new_threshold),
+      GNUNET_JSON_spec_uint32 ("new_state",
+                               &state32),
+      GNUNET_JSON_spec_fixed_auto ("decider_pub",
+                                   &aml->decider_pub),
+      GNUNET_JSON_spec_end ()
+    };
+
+    if (GNUNET_OK !=
+        GNUNET_JSON_parse (obj,
+                           spec,
+                           NULL,
+                           NULL))
+    {
+      GNUNET_break_op (0);
+      return GNUNET_SYSERR;
+    }
+    aml->new_state = (enum TALER_AmlDecisionState) state32;
+  }
+  return GNUNET_OK;
+}
+
+
+/**
+ * Parse KYC response array.
+ *
+ * @param kyc_attributes JSON array with KYC details
+ * @param[out] kyc_attributes_ar where to write the result
+ * @return #GNUNET_OK on success
+ */
+static enum GNUNET_GenericReturnValue
+parse_kyc_attributes (const json_t *kyc_attributes,
+                      struct TALER_EXCHANGE_KycHistoryDetail 
*kyc_attributes_ar)
+{
+  json_t *obj;
+  size_t idx;
+
+  json_array_foreach (kyc_attributes, idx, obj)
+  {
+    struct TALER_EXCHANGE_KycHistoryDetail *kyc = &kyc_attributes_ar[idx];
+    struct GNUNET_JSON_Specification spec[] = {
+      GNUNET_JSON_spec_timestamp ("collection_time",
+                                  &kyc->collection_time),
+      GNUNET_JSON_spec_mark_optional (
+        GNUNET_JSON_spec_object_const ("attributes",
+                                       &kyc->attributes),
+        NULL),
+      GNUNET_JSON_spec_string ("provider_section",
+                               &kyc->provider_section),
+      GNUNET_JSON_spec_end ()
+    };
+
+    if (GNUNET_OK !=
+        GNUNET_JSON_parse (obj,
+                           spec,
+                           NULL,
+                           NULL))
+    {
+      GNUNET_break_op (0);
+      return GNUNET_SYSERR;
+    }
+  }
+  return GNUNET_OK;
+}
+
+
+/**
+ * Parse the provided decision data from the "200 OK" response.
+ *
+ * @param[in,out] lh handle (callback may be zero'ed out)
+ * @param json json reply with the data for one coin
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR on error
+ */
+static enum GNUNET_GenericReturnValue
+parse_decision_ok (struct TALER_EXCHANGE_LookupAmlDecision *lh,
+                   const json_t *json)
+{
+  struct TALER_EXCHANGE_AmlDecisionResponse lr = {
+    .hr.reply = json,
+    .hr.http_status = MHD_HTTP_OK
+  };
+  const json_t *aml_history;
+  const json_t *kyc_attributes;
+  struct GNUNET_JSON_Specification spec[] = {
+    GNUNET_JSON_spec_array_const ("aml_history",
+                                  &aml_history),
+    GNUNET_JSON_spec_array_const ("kyc_attributes",
+                                  &kyc_attributes),
+    GNUNET_JSON_spec_end ()
+  };
+
+  if (GNUNET_OK !=
+      GNUNET_JSON_parse (json,
+                         spec,
+                         NULL,
+                         NULL))
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+  lr.details.ok.aml_history_length = json_array_size (aml_history);
+  lr.details.ok.kyc_attributes_length = json_array_size (kyc_attributes);
+  {
+    struct TALER_EXCHANGE_AmlDecisionDetail aml_history_ar[
+      GNUNET_NZL (lr.details.ok.aml_history_length)];
+    struct TALER_EXCHANGE_KycHistoryDetail kyc_attributes_ar[
+      GNUNET_NZL (lr.details.ok.kyc_attributes_length)];
+    enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
+
+    memset (aml_history_ar,
+            0,
+            sizeof (aml_history_ar));
+    memset (kyc_attributes_ar,
+            0,
+            sizeof (kyc_attributes_ar));
+    lr.details.ok.aml_history = aml_history_ar;
+    lr.details.ok.kyc_attributes = kyc_attributes_ar;
+    ret = parse_aml_history (aml_history,
+                             aml_history_ar);
+    if (GNUNET_OK == ret)
+      ret = parse_kyc_attributes (kyc_attributes,
+                                  kyc_attributes_ar);
+    if (GNUNET_OK == ret)
+    {
+      lh->decision_cb (lh->decision_cb_cls,
+                       &lr);
+      lh->decision_cb = NULL;
+    }
+    return ret;
+  }
+}
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /aml/$OFFICER_PUB/decision request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_LookupAmlDecision`
+ * @param response_code HTTP response code, 0 on error
+ * @param response parsed JSON result, NULL on error
+ */
+static void
+handle_lookup_finished (void *cls,
+                        long response_code,
+                        const void *response)
+{
+  struct TALER_EXCHANGE_LookupAmlDecision *lh = cls;
+  const json_t *j = response;
+  struct TALER_EXCHANGE_AmlDecisionResponse lr = {
+    .hr.reply = j,
+    .hr.http_status = (unsigned int) response_code
+  };
+
+  lh->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    lr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    break;
+  case MHD_HTTP_OK:
+    if (GNUNET_OK !=
+        parse_decision_ok (lh,
+                           j))
+    {
+      GNUNET_break_op (0);
+      lr.hr.http_status = 0;
+      lr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+      break;
+    }
+    GNUNET_assert (NULL == lh->decision_cb);
+    TALER_EXCHANGE_lookup_aml_decision_cancel (lh);
+    return;
+  case MHD_HTTP_NO_CONTENT:
+    break;
+  case MHD_HTTP_BAD_REQUEST:
+    lr.hr.ec = TALER_JSON_get_error_code (j);
+    lr.hr.hint = TALER_JSON_get_error_hint (j);
+    /* This should never happen, either us or the exchange is buggy
+       (or API version conflict); just pass JSON reply to the application */
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    lr.hr.ec = TALER_JSON_get_error_code (j);
+    lr.hr.hint = TALER_JSON_get_error_hint (j);
+    /* Nothing really to verify, exchange says this coin was not melted; we
+       should pass the JSON reply to the application */
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    lr.hr.ec = TALER_JSON_get_error_code (j);
+    lr.hr.hint = TALER_JSON_get_error_hint (j);
+    /* Nothing really to verify, exchange says this coin was not melted; we
+       should pass the JSON reply to the application */
+    break;
+  case MHD_HTTP_INTERNAL_SERVER_ERROR:
+    lr.hr.ec = TALER_JSON_get_error_code (j);
+    lr.hr.hint = TALER_JSON_get_error_hint (j);
+    /* Server had an internal issue; we should retry, but this API
+       leaves this to the application */
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    lr.hr.ec = TALER_JSON_get_error_code (j);
+    lr.hr.hint = TALER_JSON_get_error_hint (j);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange lookup AML 
decision\n",
+                (unsigned int) response_code,
+                (int) lr.hr.ec);
+    break;
+  }
+  if (NULL != lh->decision_cb)
+    lh->decision_cb (lh->decision_cb_cls,
+                     &lr);
+  TALER_EXCHANGE_lookup_aml_decision_cancel (lh);
+}
+
+
+struct TALER_EXCHANGE_LookupAmlDecision *
+TALER_EXCHANGE_lookup_aml_decision (
+  struct GNUNET_CURL_Context *ctx,
+  const char *exchange_url,
+  const struct TALER_PaytoHashP *h_payto,
+  const struct TALER_AmlOfficerPrivateKeyP *officer_priv,
+  bool history,
+  TALER_EXCHANGE_LookupAmlDecisionCallback cb,
+  void *cb_cls)
+{
+  struct TALER_EXCHANGE_LookupAmlDecision *lh;
+  CURL *eh;
+  struct TALER_AmlOfficerPublicKeyP officer_pub;
+  struct TALER_AmlOfficerSignatureP officer_sig;
+  char arg_str[sizeof (officer_pub) * 2
+               + sizeof (*h_payto) * 2 + 32];
+
+  GNUNET_CRYPTO_eddsa_key_get_public (&officer_priv->eddsa_priv,
+                                      &officer_pub.eddsa_pub);
+  TALER_officer_aml_query_sign (officer_priv,
+                                &officer_sig);
+  {
+    char pub_str[sizeof (officer_pub) * 2];
+    char pt_str[sizeof (*h_payto) * 2];
+    char *end;
+
+    end = GNUNET_STRINGS_data_to_string (
+      &officer_pub,
+      sizeof (officer_pub),
+      pub_str,
+      sizeof (pub_str));
+    *end = '\0';
+    end = GNUNET_STRINGS_data_to_string (
+      h_payto,
+      sizeof (*h_payto),
+      pt_str,
+      sizeof (pt_str));
+    *end = '\0';
+    GNUNET_snprintf (arg_str,
+                     sizeof (arg_str),
+                     "aml/%s/decision/%s",
+                     pub_str,
+                     pt_str);
+  }
+  lh = GNUNET_new (struct TALER_EXCHANGE_LookupAmlDecision);
+  lh->decision_cb = cb;
+  lh->decision_cb_cls = cb_cls;
+  lh->url = TALER_url_join (exchange_url,
+                            arg_str,
+                            "history",
+                            history
+                            ? "true"
+                            : NULL,
+                            NULL);
+  if (NULL == lh->url)
+  {
+    GNUNET_free (lh);
+    return NULL;
+  }
+  eh = TALER_EXCHANGE_curl_easy_get_ (lh->url);
+  if (NULL == eh)
+  {
+    GNUNET_break (0);
+    GNUNET_free (lh->url);
+    GNUNET_free (lh);
+    return NULL;
+  }
+  {
+    char *hdr;
+    char sig_str[sizeof (officer_sig) * 2];
+    char *end;
+
+    end = GNUNET_STRINGS_data_to_string (
+      &officer_sig,
+      sizeof (officer_sig),
+      sig_str,
+      sizeof (sig_str));
+    *end = '\0';
+
+    GNUNET_asprintf (&hdr,
+                     "%s: %s",
+                     TALER_AML_OFFICER_SIGNATURE_HEADER,
+                     sig_str);
+    lh->job_headers = curl_slist_append (NULL,
+                                         hdr);
+    GNUNET_free (hdr);
+    lh->job_headers = curl_slist_append (lh->job_headers,
+                                         "Content-type: application/json");
+    lh->job = GNUNET_CURL_job_add2 (ctx,
+                                    eh,
+                                    lh->job_headers,
+                                    &handle_lookup_finished,
+                                    lh);
+  }
+  return lh;
+}
+
+
+void
+TALER_EXCHANGE_lookup_aml_decision_cancel (
+  struct TALER_EXCHANGE_LookupAmlDecision *lh)
+{
+  if (NULL != lh->job)
+  {
+    GNUNET_CURL_job_cancel (lh->job);
+    lh->job = NULL;
+  }
+  curl_slist_free_all (lh->job_headers);
+  GNUNET_free (lh->url);
+  GNUNET_free (lh);
+}
+
+
+/* end of exchange_api_lookup_aml_decision.c */
diff --git a/src/lib/exchange_api_lookup_aml_decisions.c 
b/src/lib/exchange_api_lookup_aml_decisions.c
new file mode 100644
index 00000000..22222b1e
--- /dev/null
+++ b/src/lib/exchange_api_lookup_aml_decisions.c
@@ -0,0 +1,378 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2023 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_lookup_aml_decisions.c
+ * @brief Implementation of the /aml/$OFFICER_PUB/decisions request
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <microhttpd.h> /* just for HTTP status codes */
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_exchange_service.h"
+#include "taler_json_lib.h"
+#include "exchange_api_handle.h"
+#include "taler_signatures.h"
+#include "exchange_api_curl_defaults.h"
+
+
+/**
+ * @brief A /coins/$COIN_PUB/link Handle
+ */
+struct TALER_EXCHANGE_LookupAmlDecisions
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_LookupAmlDecisionsCallback decisions_cb;
+
+  /**
+   * Closure for @e cb.
+   */
+  void *decisions_cb_cls;
+
+  /**
+   * HTTP headers for the job.
+   */
+  struct curl_slist *job_headers;
+};
+
+
+/**
+ * Parse AML decision summary array.
+ *
+ * @param decisions JSON array with AML decision summaries
+ * @param[out] decision_ar where to write the result
+ * @return #GNUNET_OK on success
+ */
+static enum GNUNET_GenericReturnValue
+parse_aml_decisions (const json_t *decisions,
+                     struct TALER_EXCHANGE_AmlDecisionSummary *decision_ar)
+{
+  json_t *obj;
+  size_t idx;
+
+  json_array_foreach (decisions, idx, obj)
+  {
+    struct TALER_EXCHANGE_AmlDecisionSummary *decision = &decision_ar[idx];
+    uint32_t state32;
+    struct GNUNET_JSON_Specification spec[] = {
+      GNUNET_JSON_spec_fixed_auto ("h_payto",
+                                   &decision->h_payto),
+      GNUNET_JSON_spec_uint32 ("current_state",
+                               &state32),
+      TALER_JSON_spec_amount_any ("threshold",
+                                  &decision->threshold),
+      GNUNET_JSON_spec_uint64 ("rowid",
+                               &decision->rowid),
+      GNUNET_JSON_spec_end ()
+    };
+
+    if (GNUNET_OK !=
+        GNUNET_JSON_parse (obj,
+                           spec,
+                           NULL,
+                           NULL))
+    {
+      GNUNET_break_op (0);
+      return GNUNET_SYSERR;
+    }
+    decision->current_state = (enum TALER_AmlDecisionState) state32;
+  }
+  return GNUNET_OK;
+}
+
+
+/**
+ * Parse the provided decision data from the "200 OK" response.
+ *
+ * @param[in,out] lh handle (callback may be zero'ed out)
+ * @param json json reply with the data for one coin
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR on error
+ */
+static enum GNUNET_GenericReturnValue
+parse_decisions_ok (struct TALER_EXCHANGE_LookupAmlDecisions *lh,
+                    const json_t *json)
+{
+  struct TALER_EXCHANGE_AmlDecisionsResponse lr = {
+    .hr.reply = json,
+    .hr.http_status = MHD_HTTP_OK
+  };
+  const json_t *records;
+  struct GNUNET_JSON_Specification spec[] = {
+    GNUNET_JSON_spec_array_const ("records",
+                                  &records),
+    GNUNET_JSON_spec_end ()
+  };
+
+  if (GNUNET_OK !=
+      GNUNET_JSON_parse (json,
+                         spec,
+                         NULL,
+                         NULL))
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+  lr.details.ok.decisions_length = json_array_size (records);
+  {
+    struct TALER_EXCHANGE_AmlDecisionSummary decisions[
+      GNUNET_NZL (lr.details.ok.decisions_length)];
+    enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
+
+    lr.details.ok.decisions = decisions;
+    ret = parse_aml_decisions (records,
+                               decisions);
+    if (GNUNET_OK == ret)
+    {
+      lh->decisions_cb (lh->decisions_cb_cls,
+                        &lr);
+      lh->decisions_cb = NULL;
+    }
+    return ret;
+  }
+}
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /aml/$OFFICER_PUB/decisions request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_LookupAmlDecisions`
+ * @param response_code HTTP response code, 0 on error
+ * @param response parsed JSON result, NULL on error
+ */
+static void
+handle_lookup_finished (void *cls,
+                        long response_code,
+                        const void *response)
+{
+  struct TALER_EXCHANGE_LookupAmlDecisions *lh = cls;
+  const json_t *j = response;
+  struct TALER_EXCHANGE_AmlDecisionsResponse lr = {
+    .hr.reply = j,
+    .hr.http_status = (unsigned int) response_code
+  };
+
+  lh->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    lr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    break;
+  case MHD_HTTP_OK:
+    if (GNUNET_OK !=
+        parse_decisions_ok (lh,
+                            j))
+    {
+      GNUNET_break_op (0);
+      lr.hr.http_status = 0;
+      lr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+      break;
+    }
+    GNUNET_assert (NULL == lh->decisions_cb);
+    TALER_EXCHANGE_lookup_aml_decisions_cancel (lh);
+    return;
+  case MHD_HTTP_NO_CONTENT:
+    break;
+  case MHD_HTTP_BAD_REQUEST:
+    lr.hr.ec = TALER_JSON_get_error_code (j);
+    lr.hr.hint = TALER_JSON_get_error_hint (j);
+    /* This should never happen, either us or the exchange is buggy
+       (or API version conflict); just pass JSON reply to the application */
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    lr.hr.ec = TALER_JSON_get_error_code (j);
+    lr.hr.hint = TALER_JSON_get_error_hint (j);
+    /* Nothing really to verify, exchange says this coin was not melted; we
+       should pass the JSON reply to the application */
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    lr.hr.ec = TALER_JSON_get_error_code (j);
+    lr.hr.hint = TALER_JSON_get_error_hint (j);
+    /* Nothing really to verify, exchange says this coin was not melted; we
+       should pass the JSON reply to the application */
+    break;
+  case MHD_HTTP_INTERNAL_SERVER_ERROR:
+    lr.hr.ec = TALER_JSON_get_error_code (j);
+    lr.hr.hint = TALER_JSON_get_error_hint (j);
+    /* Server had an internal issue; we should retry, but this API
+       leaves this to the application */
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    lr.hr.ec = TALER_JSON_get_error_code (j);
+    lr.hr.hint = TALER_JSON_get_error_hint (j);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for lookup AML decisions\n",
+                (unsigned int) response_code,
+                (int) lr.hr.ec);
+    break;
+  }
+  if (NULL != lh->decisions_cb)
+    lh->decisions_cb (lh->decisions_cb_cls,
+                      &lr);
+  TALER_EXCHANGE_lookup_aml_decisions_cancel (lh);
+}
+
+
+struct TALER_EXCHANGE_LookupAmlDecisions *
+TALER_EXCHANGE_lookup_aml_decisions (
+  struct GNUNET_CURL_Context *ctx,
+  const char *exchange_url,
+  uint64_t start,
+  int delta,
+  enum TALER_AmlDecisionState state,
+  const struct TALER_AmlOfficerPrivateKeyP *officer_priv,
+  TALER_EXCHANGE_LookupAmlDecisionsCallback cb,
+  void *cb_cls)
+{
+  struct TALER_EXCHANGE_LookupAmlDecisions *lh;
+  CURL *eh;
+  struct TALER_AmlOfficerPublicKeyP officer_pub;
+  struct TALER_AmlOfficerSignatureP officer_sig;
+  char arg_str[sizeof (struct TALER_AmlOfficerPublicKeyP) * 2 + 32];
+  const char *state_str = NULL;
+
+  switch (state)
+  {
+  case TALER_AML_NORMAL:
+    state_str = "normal";
+    break;
+  case TALER_AML_PENDING:
+    state_str = "pending";
+    break;
+  case TALER_AML_FROZEN:
+    state_str = "frozen";
+    break;
+  }
+  GNUNET_assert (NULL != state_str);
+  GNUNET_CRYPTO_eddsa_key_get_public (&officer_priv->eddsa_priv,
+                                      &officer_pub.eddsa_pub);
+  TALER_officer_aml_query_sign (officer_priv,
+                                &officer_sig);
+  {
+    char pub_str[sizeof (officer_pub) * 2];
+    char *end;
+
+    end = GNUNET_STRINGS_data_to_string (
+      &officer_pub,
+      sizeof (officer_pub),
+      pub_str,
+      sizeof (pub_str));
+    *end = '\0';
+    GNUNET_snprintf (arg_str,
+                     sizeof (arg_str),
+                     "aml/%s/decisions/%s",
+                     pub_str,
+                     state_str);
+  }
+  lh = GNUNET_new (struct TALER_EXCHANGE_LookupAmlDecisions);
+  lh->decisions_cb = cb;
+  lh->decisions_cb_cls = cb_cls;
+  {
+    char delta_s[24];
+    char start_s[24];
+
+    GNUNET_snprintf (delta_s,
+                     sizeof (delta_s),
+                     "%d",
+                     delta);
+    GNUNET_snprintf (start_s,
+                     sizeof (start_s),
+                     "%llu",
+                     (unsigned long long) start);
+    lh->url = TALER_url_join (exchange_url,
+                              arg_str,
+                              "delta",
+                              delta_s,
+                              "start",
+                              start_s,
+                              NULL);
+  }
+  if (NULL == lh->url)
+  {
+    GNUNET_free (lh);
+    return NULL;
+  }
+  eh = TALER_EXCHANGE_curl_easy_get_ (lh->url);
+  if (NULL == eh)
+  {
+    GNUNET_break (0);
+    GNUNET_free (lh->url);
+    GNUNET_free (lh);
+    return NULL;
+  }
+  {
+    char *hdr;
+    char sig_str[sizeof (officer_sig) * 2];
+    char *end;
+
+    end = GNUNET_STRINGS_data_to_string (
+      &officer_sig,
+      sizeof (officer_sig),
+      sig_str,
+      sizeof (sig_str));
+    *end = '\0';
+
+    GNUNET_asprintf (&hdr,
+                     "%s: %s",
+                     TALER_AML_OFFICER_SIGNATURE_HEADER,
+                     sig_str);
+    lh->job_headers = curl_slist_append (NULL,
+                                         hdr);
+    GNUNET_free (hdr);
+    lh->job_headers = curl_slist_append (lh->job_headers,
+                                         "Content-type: application/json");
+    lh->job = GNUNET_CURL_job_add2 (ctx,
+                                    eh,
+                                    lh->job_headers,
+                                    &handle_lookup_finished,
+                                    lh);
+  }
+  return lh;
+}
+
+
+void
+TALER_EXCHANGE_lookup_aml_decisions_cancel (
+  struct TALER_EXCHANGE_LookupAmlDecisions *lh)
+{
+  if (NULL != lh->job)
+  {
+    GNUNET_CURL_job_cancel (lh->job);
+    lh->job = NULL;
+  }
+  curl_slist_free_all (lh->job_headers);
+  GNUNET_free (lh->url);
+  GNUNET_free (lh);
+}
+
+
+/* end of exchange_api_lookup_aml_decisions.c */
diff --git a/src/lib/exchange_api_management_add_partner.c 
b/src/lib/exchange_api_management_add_partner.c
new file mode 100644
index 00000000..fec66c56
--- /dev/null
+++ b/src/lib/exchange_api_management_add_partner.c
@@ -0,0 +1,218 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2023 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_management_add_partner.c
+ * @brief functions to add an partner by an AML officer
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "taler_json_lib.h"
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_exchange_service.h"
+#include "exchange_api_curl_defaults.h"
+#include "taler_signatures.h"
+#include "taler_curl_lib.h"
+#include "taler_json_lib.h"
+
+
+struct TALER_EXCHANGE_ManagementAddPartner
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Minor context that holds body and headers.
+   */
+  struct TALER_CURL_PostContext post_ctx;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_ManagementAddPartnerCallback cb;
+
+  /**
+   * Closure for @a cb.
+   */
+  void *cb_cls;
+
+  /**
+   * Reference to the execution context.
+   */
+  struct GNUNET_CURL_Context *ctx;
+};
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP POST /management/partners request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_ManagementAddPartner *`
+ * @param response_code HTTP response code, 0 on error
+ * @param response response body, NULL if not in JSON
+ */
+static void
+handle_add_partner_finished (void *cls,
+                             long response_code,
+                             const void *response)
+{
+  struct TALER_EXCHANGE_ManagementAddPartner *wh = cls;
+  const json_t *json = response;
+  struct TALER_EXCHANGE_ManagementAddPartnerResponse apr = {
+    .hr.http_status = (unsigned int) response_code,
+    .hr.reply = json
+  };
+
+  wh->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    /* no reply */
+    apr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    apr.hr.hint = "server offline?";
+    break;
+  case MHD_HTTP_NO_CONTENT:
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    apr.hr.ec = TALER_JSON_get_error_code (json);
+    apr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_CONFLICT:
+    apr.hr.ec = TALER_JSON_get_error_code (json);
+    apr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    apr.hr.ec = TALER_JSON_get_error_code (json);
+    apr.hr.hint = TALER_JSON_get_error_hint (json);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for adding exchange partner\n",
+                (unsigned int) response_code,
+                (int) apr.hr.ec);
+    break;
+  }
+  if (NULL != wh->cb)
+  {
+    wh->cb (wh->cb_cls,
+            &apr);
+    wh->cb = NULL;
+  }
+  TALER_EXCHANGE_management_add_partner_cancel (wh);
+}
+
+
+struct TALER_EXCHANGE_ManagementAddPartner *
+TALER_EXCHANGE_management_add_partner (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  const struct TALER_MasterPublicKeyP *partner_pub,
+  struct GNUNET_TIME_Timestamp start_date,
+  struct GNUNET_TIME_Timestamp end_date,
+  struct GNUNET_TIME_Relative wad_frequency,
+  const struct TALER_Amount *wad_fee,
+  const char *partner_base_url,
+  const struct TALER_MasterSignatureP *master_sig,
+  TALER_EXCHANGE_ManagementAddPartnerCallback cb,
+  void *cb_cls)
+{
+  struct TALER_EXCHANGE_ManagementAddPartner *wh;
+  CURL *eh;
+  json_t *body;
+
+  wh = GNUNET_new (struct TALER_EXCHANGE_ManagementAddPartner);
+  wh->cb = cb;
+  wh->cb_cls = cb_cls;
+  wh->ctx = ctx;
+  wh->url = TALER_url_join (url,
+                            "management/partners",
+                            NULL);
+  if (NULL == wh->url)
+  {
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Could not construct request URL.\n");
+    GNUNET_free (wh);
+    return NULL;
+  }
+  body = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_string ("partner_base_url",
+                             partner_base_url),
+    GNUNET_JSON_pack_timestamp ("start_date",
+                                start_date),
+    GNUNET_JSON_pack_timestamp ("end_date",
+                                end_date),
+    GNUNET_JSON_pack_time_rel ("wad_frequency",
+                               wad_frequency),
+    GNUNET_JSON_pack_data_auto ("partner_pub",
+                                &partner_pub),
+    GNUNET_JSON_pack_data_auto ("master_sig",
+                                &master_sig),
+    TALER_JSON_pack_amount ("wad_fee",
+                            wad_fee)
+    );
+  eh = TALER_EXCHANGE_curl_easy_get_ (wh->url);
+  if ( (NULL == eh) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&wh->post_ctx,
+                              eh,
+                              body)) )
+  {
+    GNUNET_break (0);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    json_decref (body);
+    GNUNET_free (wh->url);
+    return NULL;
+  }
+  json_decref (body);
+  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+              "Requesting URL '%s'\n",
+              wh->url);
+  wh->job = GNUNET_CURL_job_add2 (ctx,
+                                  eh,
+                                  wh->post_ctx.headers,
+                                  &handle_add_partner_finished,
+                                  wh);
+  if (NULL == wh->job)
+  {
+    TALER_EXCHANGE_management_add_partner_cancel (wh);
+    return NULL;
+  }
+  return wh;
+}
+
+
+void
+TALER_EXCHANGE_management_add_partner_cancel (
+  struct TALER_EXCHANGE_ManagementAddPartner *wh)
+{
+  if (NULL != wh->job)
+  {
+    GNUNET_CURL_job_cancel (wh->job);
+    wh->job = NULL;
+  }
+  TALER_curl_easy_post_finished (&wh->post_ctx);
+  GNUNET_free (wh->url);
+  GNUNET_free (wh);
+}
diff --git a/src/lib/exchange_api_management_auditor_disable.c 
b/src/lib/exchange_api_management_auditor_disable.c
new file mode 100644
index 00000000..8bce7f74
--- /dev/null
+++ b/src/lib/exchange_api_management_auditor_disable.c
@@ -0,0 +1,219 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2015-2021 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_management_auditor_disable.c
+ * @brief functions to disable an auditor
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "taler_json_lib.h"
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_exchange_service.h"
+#include "exchange_api_curl_defaults.h"
+#include "taler_signatures.h"
+#include "taler_curl_lib.h"
+#include "taler_json_lib.h"
+
+/**
+ * @brief Handle for a POST /management/auditors/$AUDITOR_PUB/disable request.
+ */
+struct TALER_EXCHANGE_ManagementAuditorDisableHandle
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Minor context that holds body and headers.
+   */
+  struct TALER_CURL_PostContext post_ctx;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_ManagementAuditorDisableCallback cb;
+
+  /**
+   * Closure for @a cb.
+   */
+  void *cb_cls;
+
+  /**
+   * Reference to the execution context.
+   */
+  struct GNUNET_CURL_Context *ctx;
+};
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /management/auditors/%s/disable request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_ManagementAuditorDisableHandle *`
+ * @param response_code HTTP response code, 0 on error
+ * @param response response body, NULL if not in JSON
+ */
+static void
+handle_auditor_disable_finished (void *cls,
+                                 long response_code,
+                                 const void *response)
+{
+  struct TALER_EXCHANGE_ManagementAuditorDisableHandle *ah = cls;
+  const json_t *json = response;
+  struct TALER_EXCHANGE_ManagementAuditorDisableResponse adr = {
+    .hr.http_status = (unsigned int) response_code,
+    .hr.reply = json
+  };
+
+  ah->job = NULL;
+  switch (response_code)
+  {
+  case MHD_HTTP_NO_CONTENT:
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    adr.hr.ec = TALER_JSON_get_error_code (json);
+    adr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    adr.hr.ec = TALER_JSON_get_error_code (json);
+    adr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_CONFLICT:
+    adr.hr.ec = TALER_JSON_get_error_code (json);
+    adr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    adr.hr.ec = TALER_JSON_get_error_code (json);
+    adr.hr.hint = TALER_JSON_get_error_hint (json);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange management 
auditor disable\n",
+                (unsigned int) response_code,
+                (int) adr.hr.ec);
+    break;
+  }
+  if (NULL != ah->cb)
+  {
+    ah->cb (ah->cb_cls,
+            &adr);
+    ah->cb = NULL;
+  }
+  TALER_EXCHANGE_management_disable_auditor_cancel (ah);
+}
+
+
+struct TALER_EXCHANGE_ManagementAuditorDisableHandle *
+TALER_EXCHANGE_management_disable_auditor (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  const struct TALER_AuditorPublicKeyP *auditor_pub,
+  struct GNUNET_TIME_Timestamp validity_end,
+  const struct TALER_MasterSignatureP *master_sig,
+  TALER_EXCHANGE_ManagementAuditorDisableCallback cb,
+  void *cb_cls)
+{
+  struct TALER_EXCHANGE_ManagementAuditorDisableHandle *ah;
+  CURL *eh;
+  json_t *body;
+
+  ah = GNUNET_new (struct TALER_EXCHANGE_ManagementAuditorDisableHandle);
+  ah->cb = cb;
+  ah->cb_cls = cb_cls;
+  ah->ctx = ctx;
+  {
+    char epub_str[sizeof (*auditor_pub) * 2];
+    char arg_str[sizeof (epub_str) + 64];
+    char *end;
+
+    end = GNUNET_STRINGS_data_to_string (auditor_pub,
+                                         sizeof (*auditor_pub),
+                                         epub_str,
+                                         sizeof (epub_str));
+    *end = '\0';
+    GNUNET_snprintf (arg_str,
+                     sizeof (arg_str),
+                     "management/auditors/%s/disable",
+                     epub_str);
+    ah->url = TALER_url_join (url,
+                              arg_str,
+                              NULL);
+  }
+  if (NULL == ah->url)
+  {
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Could not construct request URL.\n");
+    GNUNET_free (ah);
+    return NULL;
+  }
+  body = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_data_auto ("master_sig",
+                                master_sig),
+    GNUNET_JSON_pack_timestamp ("validity_end",
+                                validity_end));
+  eh = TALER_EXCHANGE_curl_easy_get_ (ah->url);
+  if ( (NULL == eh) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&ah->post_ctx,
+                              eh,
+                              body)))
+  {
+    GNUNET_break (0);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    json_decref (body);
+    GNUNET_free (ah->url);
+    return NULL;
+  }
+  json_decref (body);
+  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+              "Requesting URL '%s'\n",
+              ah->url);
+  ah->job = GNUNET_CURL_job_add2 (ctx,
+                                  eh,
+                                  ah->post_ctx.headers,
+                                  &handle_auditor_disable_finished,
+                                  ah);
+  if (NULL == ah->job)
+  {
+    TALER_EXCHANGE_management_disable_auditor_cancel (ah);
+    return NULL;
+  }
+  return ah;
+}
+
+
+void
+TALER_EXCHANGE_management_disable_auditor_cancel (
+  struct TALER_EXCHANGE_ManagementAuditorDisableHandle *ah)
+{
+  if (NULL != ah->job)
+  {
+    GNUNET_CURL_job_cancel (ah->job);
+    ah->job = NULL;
+  }
+  TALER_curl_easy_post_finished (&ah->post_ctx);
+  GNUNET_free (ah->url);
+  GNUNET_free (ah);
+}
diff --git a/src/lib/exchange_api_management_auditor_enable.c 
b/src/lib/exchange_api_management_auditor_enable.c
new file mode 100644
index 00000000..41c5049c
--- /dev/null
+++ b/src/lib/exchange_api_management_auditor_enable.c
@@ -0,0 +1,224 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2015-2021 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_management_auditor_enable.c
+ * @brief functions to enable an auditor
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "taler_json_lib.h"
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_exchange_service.h"
+#include "exchange_api_curl_defaults.h"
+#include "taler_signatures.h"
+#include "taler_curl_lib.h"
+#include "taler_json_lib.h"
+
+
+/**
+ * @brief Handle for a POST /management/auditors request.
+ */
+struct TALER_EXCHANGE_ManagementAuditorEnableHandle
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Minor context that holds body and headers.
+   */
+  struct TALER_CURL_PostContext post_ctx;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_ManagementAuditorEnableCallback cb;
+
+  /**
+   * Closure for @a cb.
+   */
+  void *cb_cls;
+
+  /**
+   * Reference to the execution context.
+   */
+  struct GNUNET_CURL_Context *ctx;
+};
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP POST /management/auditors request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_ManagementAuditorEnableHandle *`
+ * @param response_code HTTP response code, 0 on error
+ * @param response response body, NULL if not in JSON
+ */
+static void
+handle_auditor_enable_finished (void *cls,
+                                long response_code,
+                                const void *response)
+{
+  struct TALER_EXCHANGE_ManagementAuditorEnableHandle *ah = cls;
+  const json_t *json = response;
+  struct TALER_EXCHANGE_ManagementAuditorEnableResponse aer = {
+    .hr.http_status = (unsigned int) response_code,
+    .hr.reply = json
+  };
+
+  ah->job = NULL;
+  switch (response_code)
+  {
+  case MHD_HTTP_NO_CONTENT:
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    aer.hr.ec = TALER_JSON_get_error_code (json);
+    aer.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Server did not find handler at `%s'. Did you configure the 
correct exchange base URL?\n",
+                ah->url);
+    if (NULL != json)
+    {
+      aer.hr.ec = TALER_JSON_get_error_code (json);
+      aer.hr.hint = TALER_JSON_get_error_hint (json);
+    }
+    else
+    {
+      aer.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+      aer.hr.hint = TALER_ErrorCode_get_hint (aer.hr.ec);
+    }
+    break;
+  case MHD_HTTP_CONFLICT:
+    aer.hr.ec = TALER_JSON_get_error_code (json);
+    aer.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    aer.hr.ec = TALER_JSON_get_error_code (json);
+    aer.hr.hint = TALER_JSON_get_error_hint (json);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange management 
auditor enable\n",
+                (unsigned int) response_code,
+                (int) aer.hr.ec);
+    break;
+  }
+  if (NULL != ah->cb)
+  {
+    ah->cb (ah->cb_cls,
+            &aer);
+    ah->cb = NULL;
+  }
+  TALER_EXCHANGE_management_enable_auditor_cancel (ah);
+}
+
+
+struct TALER_EXCHANGE_ManagementAuditorEnableHandle *
+TALER_EXCHANGE_management_enable_auditor (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  const struct TALER_AuditorPublicKeyP *auditor_pub,
+  const char *auditor_url,
+  const char *auditor_name,
+  struct GNUNET_TIME_Timestamp validity_start,
+  const struct TALER_MasterSignatureP *master_sig,
+  TALER_EXCHANGE_ManagementAuditorEnableCallback cb,
+  void *cb_cls)
+{
+  struct TALER_EXCHANGE_ManagementAuditorEnableHandle *ah;
+  CURL *eh;
+  json_t *body;
+
+  ah = GNUNET_new (struct TALER_EXCHANGE_ManagementAuditorEnableHandle);
+  ah->cb = cb;
+  ah->cb_cls = cb_cls;
+  ah->ctx = ctx;
+  ah->url = TALER_url_join (url,
+                            "management/auditors",
+                            NULL);
+  if (NULL == ah->url)
+  {
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Could not construct request URL.\n");
+    GNUNET_free (ah);
+    return NULL;
+  }
+  body = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_string ("auditor_url",
+                             auditor_url),
+    GNUNET_JSON_pack_string ("auditor_name",
+                             auditor_name),
+    GNUNET_JSON_pack_data_auto ("auditor_pub",
+                                auditor_pub),
+    GNUNET_JSON_pack_data_auto ("master_sig",
+                                master_sig),
+    GNUNET_JSON_pack_timestamp ("validity_start",
+                                validity_start));
+  eh = TALER_EXCHANGE_curl_easy_get_ (ah->url);
+  if ( (NULL == eh) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&ah->post_ctx,
+                              eh,
+                              body)) )
+  {
+    GNUNET_break (0);
+    json_decref (body);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    GNUNET_free (ah->url);
+    return NULL;
+  }
+  json_decref (body);
+  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+              "Requesting URL '%s'\n",
+              ah->url);
+  ah->job = GNUNET_CURL_job_add2 (ctx,
+                                  eh,
+                                  ah->post_ctx.headers,
+                                  &handle_auditor_enable_finished,
+                                  ah);
+  if (NULL == ah->job)
+  {
+    TALER_EXCHANGE_management_enable_auditor_cancel (ah);
+    return NULL;
+  }
+  return ah;
+}
+
+
+void
+TALER_EXCHANGE_management_enable_auditor_cancel (
+  struct TALER_EXCHANGE_ManagementAuditorEnableHandle *ah)
+{
+  if (NULL != ah->job)
+  {
+    GNUNET_CURL_job_cancel (ah->job);
+    ah->job = NULL;
+  }
+  TALER_curl_easy_post_finished (&ah->post_ctx);
+  GNUNET_free (ah->url);
+  GNUNET_free (ah);
+}
diff --git a/src/lib/exchange_api_management_drain_profits.c 
b/src/lib/exchange_api_management_drain_profits.c
new file mode 100644
index 00000000..bc7232b8
--- /dev/null
+++ b/src/lib/exchange_api_management_drain_profits.c
@@ -0,0 +1,213 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2020-2023 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_management_drain_profits.c
+ * @brief functions to set wire fees at an exchange
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "taler_json_lib.h"
+#include <gnunet/gnunet_curl_lib.h>
+#include "exchange_api_curl_defaults.h"
+#include "taler_exchange_service.h"
+#include "taler_signatures.h"
+#include "taler_curl_lib.h"
+#include "taler_json_lib.h"
+
+
+struct TALER_EXCHANGE_ManagementDrainProfitsHandle
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Minor context that holds body and headers.
+   */
+  struct TALER_CURL_PostContext post_ctx;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_ManagementDrainProfitsCallback cb;
+
+  /**
+   * Closure for @a cb.
+   */
+  void *cb_cls;
+
+  /**
+   * Reference to the execution context.
+   */
+  struct GNUNET_CURL_Context *ctx;
+};
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /management/drain request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_ManagementDrainProfitsHandle *`
+ * @param response_code HTTP response code, 0 on error
+ * @param response response body, NULL if not in JSON
+ */
+static void
+handle_drain_profits_finished (void *cls,
+                               long response_code,
+                               const void *response)
+{
+  struct TALER_EXCHANGE_ManagementDrainProfitsHandle *dp = cls;
+  const json_t *json = response;
+  struct TALER_EXCHANGE_ManagementDrainResponse dr = {
+    .hr.http_status = (unsigned int) response_code,
+    .hr.reply = json
+  };
+
+  dp->job = NULL;
+  switch (response_code)
+  {
+  case MHD_HTTP_NO_CONTENT:
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    dr.hr.ec = TALER_JSON_get_error_code (json);
+    dr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_CONFLICT:
+    dr.hr.ec = TALER_JSON_get_error_code (json);
+    dr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_PRECONDITION_FAILED:
+    dr.hr.ec = TALER_JSON_get_error_code (json);
+    dr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    dr.hr.ec = TALER_JSON_get_error_code (json);
+    dr.hr.hint = TALER_JSON_get_error_hint (json);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange management drain 
profits\n",
+                (unsigned int) response_code,
+                (int) dr.hr.ec);
+    break;
+  }
+  if (NULL != dp->cb)
+  {
+    dp->cb (dp->cb_cls,
+            &dr);
+    dp->cb = NULL;
+  }
+  TALER_EXCHANGE_management_drain_profits_cancel (dp);
+}
+
+
+struct TALER_EXCHANGE_ManagementDrainProfitsHandle *
+TALER_EXCHANGE_management_drain_profits (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  const struct TALER_WireTransferIdentifierRawP *wtid,
+  const struct TALER_Amount *amount,
+  struct GNUNET_TIME_Timestamp date,
+  const char *account_section,
+  const char *payto_uri,
+  const struct TALER_MasterSignatureP *master_sig,
+  TALER_EXCHANGE_ManagementDrainProfitsCallback cb,
+  void *cb_cls)
+{
+  struct TALER_EXCHANGE_ManagementDrainProfitsHandle *dp;
+  CURL *eh;
+  json_t *body;
+
+  dp = GNUNET_new (struct TALER_EXCHANGE_ManagementDrainProfitsHandle);
+  dp->cb = cb;
+  dp->cb_cls = cb_cls;
+  dp->ctx = ctx;
+  dp->url = TALER_url_join (url,
+                            "management/drain",
+                            NULL);
+  if (NULL == dp->url)
+  {
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Could not construct request URL.\n");
+    GNUNET_free (dp);
+    return NULL;
+  }
+  body = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_string ("debit_account_section",
+                             account_section),
+    GNUNET_JSON_pack_string ("credit_payto_uri",
+                             payto_uri),
+    GNUNET_JSON_pack_data_auto ("wtid",
+                                wtid),
+    GNUNET_JSON_pack_data_auto ("master_sig",
+                                master_sig),
+    GNUNET_JSON_pack_timestamp ("date",
+                                date),
+    TALER_JSON_pack_amount ("amount",
+                            amount));
+  eh = TALER_EXCHANGE_curl_easy_get_ (dp->url);
+  if ( (NULL == eh) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&dp->post_ctx,
+                              eh,
+                              body)) )
+  {
+    GNUNET_break (0);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    json_decref (body);
+    GNUNET_free (dp->url);
+    return NULL;
+  }
+  json_decref (body);
+  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+              "Requesting URL '%s'\n",
+              dp->url);
+  dp->job = GNUNET_CURL_job_add2 (ctx,
+                                  eh,
+                                  dp->post_ctx.headers,
+                                  &handle_drain_profits_finished,
+                                  dp);
+  if (NULL == dp->job)
+  {
+    TALER_EXCHANGE_management_drain_profits_cancel (dp);
+    return NULL;
+  }
+  return dp;
+}
+
+
+void
+TALER_EXCHANGE_management_drain_profits_cancel (
+  struct TALER_EXCHANGE_ManagementDrainProfitsHandle *dp)
+{
+  if (NULL != dp->job)
+  {
+    GNUNET_CURL_job_cancel (dp->job);
+    dp->job = NULL;
+  }
+  TALER_curl_easy_post_finished (&dp->post_ctx);
+  GNUNET_free (dp->url);
+  GNUNET_free (dp);
+}
diff --git a/src/lib/exchange_api_management_post_extensions.c 
b/src/lib/exchange_api_management_post_extensions.c
new file mode 100644
index 00000000..00d1c5e3
--- /dev/null
+++ b/src/lib/exchange_api_management_post_extensions.c
@@ -0,0 +1,213 @@
+/*
+   This file is part of TALER
+   Copyright (C) 2015-2023 Taler Systems SA
+
+   TALER is free software; you can redistribute it and/or modify it under the
+   terms of the GNU General Public License as published by the Free Software
+   Foundation; either version 3, or (at your option) any later version.
+
+   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+   A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License along with
+   TALER; see the file COPYING.  If not, see
+   <http://www.gnu.org/licenses/>
+ */
+/**
+ * @file lib/exchange_api_management_post_extensions.c
+ * @brief functions to handle the settings for extensions (p2p and age 
restriction)
+ * @author Özgür Kesim
+ */
+#include "platform.h"
+#include "taler_json_lib.h"
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_extensions.h"
+#include "exchange_api_curl_defaults.h"
+#include "taler_exchange_service.h"
+#include "taler_signatures.h"
+#include "taler_curl_lib.h"
+#include "taler_json_lib.h"
+
+
+/**
+ * @brief Handle for a POST /management/extensions request.
+ */
+struct TALER_EXCHANGE_ManagementPostExtensionsHandle
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Minor context that holds body and headers.
+   */
+  struct TALER_CURL_PostContext post_ctx;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_ManagementPostExtensionsCallback cb;
+
+  /**
+   * Closure for @a cb.
+   */
+  void *cb_cls;
+
+  /**
+   * Reference to the execution context.
+   */
+  struct GNUNET_CURL_Context *ctx;
+};
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP POST /management/extensions request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_ManagementPostExtensionsHandle *`
+ * @param response_code HTTP response code, 0 on error
+ * @param response response body, NULL if not in JSON
+ */
+static void
+handle_post_extensions_finished (void *cls,
+                                 long response_code,
+                                 const void *response)
+{
+  struct TALER_EXCHANGE_ManagementPostExtensionsHandle *ph = cls;
+  const json_t *json = response;
+  struct TALER_EXCHANGE_ManagementPostExtensionsResponse per = {
+    .hr.http_status = (unsigned int) response_code,
+    .hr.reply = json
+  };
+
+  ph->job = NULL;
+  switch (response_code)
+  {
+  case MHD_HTTP_NO_CONTENT:
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    per.hr.ec = TALER_JSON_get_error_code (json);
+    per.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Server did not find handler at `%s'. Did you configure the 
correct exchange base URL?\n",
+                ph->url);
+    if (NULL != json)
+    {
+      per.hr.ec = TALER_JSON_get_error_code (json);
+      per.hr.hint = TALER_JSON_get_error_hint (json);
+    }
+    else
+    {
+      per.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+      per.hr.hint = TALER_ErrorCode_get_hint (per.hr.ec);
+    }
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    per.hr.ec = TALER_JSON_get_error_code (json);
+    per.hr.hint = TALER_JSON_get_error_hint (json);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange management post 
extensions\n",
+                (unsigned int) response_code,
+                (int) per.hr.ec);
+    break;
+  }
+  if (NULL != ph->cb)
+  {
+    ph->cb (ph->cb_cls,
+            &per);
+    ph->cb = NULL;
+  }
+  TALER_EXCHANGE_management_post_extensions_cancel (ph);
+}
+
+
+struct TALER_EXCHANGE_ManagementPostExtensionsHandle *
+TALER_EXCHANGE_management_post_extensions (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  const struct TALER_EXCHANGE_ManagementPostExtensionsData *ped,
+  TALER_EXCHANGE_ManagementPostExtensionsCallback cb,
+  void *cb_cls)
+{
+  struct TALER_EXCHANGE_ManagementPostExtensionsHandle *ph;
+  CURL *eh = NULL;
+  json_t *body = NULL;
+
+  ph = GNUNET_new (struct TALER_EXCHANGE_ManagementPostExtensionsHandle);
+  ph->cb = cb;
+  ph->cb_cls = cb_cls;
+  ph->ctx = ctx;
+  ph->url = TALER_url_join (url,
+                            "management/extensions",
+                            NULL);
+  if (NULL == ph->url)
+  {
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Could not construct request URL.\n");
+    GNUNET_free (ph);
+    return NULL;
+  }
+
+  body = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_object_steal ("extensions",
+                                   (json_t *) ped->extensions),
+    GNUNET_JSON_pack_data_auto ("extensions_sig",
+                                &ped->extensions_sig));
+
+  eh = TALER_EXCHANGE_curl_easy_get_ (ph->url);
+  if ( (NULL == eh) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&ph->post_ctx,
+                              eh,
+                              body)) )
+  {
+    GNUNET_break (0);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    json_decref (body);
+    GNUNET_free (ph->url);
+    return NULL;
+  }
+  json_decref (body);
+  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+              "Requesting URL '%s'\n",
+              ph->url);
+  ph->job = GNUNET_CURL_job_add2 (ctx,
+                                  eh,
+                                  ph->post_ctx.headers,
+                                  &handle_post_extensions_finished,
+                                  ph);
+  if (NULL == ph->job)
+  {
+    TALER_EXCHANGE_management_post_extensions_cancel (ph);
+    return NULL;
+  }
+  return ph;
+}
+
+
+void
+TALER_EXCHANGE_management_post_extensions_cancel (
+  struct TALER_EXCHANGE_ManagementPostExtensionsHandle *ph)
+{
+  if (NULL != ph->job)
+  {
+    GNUNET_CURL_job_cancel (ph->job);
+    ph->job = NULL;
+  }
+  TALER_curl_easy_post_finished (&ph->post_ctx);
+  GNUNET_free (ph->url);
+  GNUNET_free (ph);
+}
diff --git a/src/lib/exchange_api_management_post_keys.c 
b/src/lib/exchange_api_management_post_keys.c
new file mode 100644
index 00000000..a46124d9
--- /dev/null
+++ b/src/lib/exchange_api_management_post_keys.c
@@ -0,0 +1,237 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2015-2021 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_management_post_keys.c
+ * @brief functions to affirm the validity of exchange keys using the master 
private key
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "taler_json_lib.h"
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_exchange_service.h"
+#include "exchange_api_curl_defaults.h"
+#include "taler_signatures.h"
+#include "taler_curl_lib.h"
+#include "taler_json_lib.h"
+
+
+/**
+ * @brief Handle for a POST /management/keys request.
+ */
+struct TALER_EXCHANGE_ManagementPostKeysHandle
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Minor context that holds body and headers.
+   */
+  struct TALER_CURL_PostContext post_ctx;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_ManagementPostKeysCallback cb;
+
+  /**
+   * Closure for @a cb.
+   */
+  void *cb_cls;
+
+  /**
+   * Reference to the execution context.
+   */
+  struct GNUNET_CURL_Context *ctx;
+};
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP POST /management/keys request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_ManagementPostKeysHandle *`
+ * @param response_code HTTP response code, 0 on error
+ * @param response response body, NULL if not in JSON
+ */
+static void
+handle_post_keys_finished (void *cls,
+                           long response_code,
+                           const void *response)
+{
+  struct TALER_EXCHANGE_ManagementPostKeysHandle *ph = cls;
+  const json_t *json = response;
+  struct TALER_EXCHANGE_ManagementPostKeysResponse pkr = {
+    .hr.http_status = (unsigned int) response_code,
+    .hr.reply = json
+  };
+
+  ph->job = NULL;
+  switch (response_code)
+  {
+  case MHD_HTTP_NO_CONTENT:
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    pkr.hr.ec = TALER_JSON_get_error_code (json);
+    pkr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    pkr.hr.ec = TALER_JSON_get_error_code (json);
+    pkr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_REQUEST_ENTITY_TOO_LARGE:
+    pkr.hr.ec = TALER_JSON_get_error_code (json);
+    pkr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    pkr.hr.ec = TALER_JSON_get_error_code (json);
+    pkr.hr.hint = TALER_JSON_get_error_hint (json);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange management post 
keys\n",
+                (unsigned int) response_code,
+                (int) pkr.hr.ec);
+    break;
+  }
+  if (NULL != ph->cb)
+  {
+    ph->cb (ph->cb_cls,
+            &pkr);
+    ph->cb = NULL;
+  }
+  TALER_EXCHANGE_post_management_keys_cancel (ph);
+}
+
+
+struct TALER_EXCHANGE_ManagementPostKeysHandle *
+TALER_EXCHANGE_post_management_keys (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  const struct TALER_EXCHANGE_ManagementPostKeysData *pkd,
+  TALER_EXCHANGE_ManagementPostKeysCallback cb,
+  void *cb_cls)
+{
+  struct TALER_EXCHANGE_ManagementPostKeysHandle *ph;
+  CURL *eh;
+  json_t *body;
+  json_t *denom_sigs;
+  json_t *signkey_sigs;
+
+  ph = GNUNET_new (struct TALER_EXCHANGE_ManagementPostKeysHandle);
+  ph->cb = cb;
+  ph->cb_cls = cb_cls;
+  ph->ctx = ctx;
+  ph->url = TALER_url_join (url,
+                            "management/keys",
+                            NULL);
+  if (NULL == ph->url)
+  {
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Could not construct request URL.\n");
+    GNUNET_free (ph);
+    return NULL;
+  }
+  denom_sigs = json_array ();
+  GNUNET_assert (NULL != denom_sigs);
+  for (unsigned int i = 0; i<pkd->num_denom_sigs; i++)
+  {
+    const struct TALER_EXCHANGE_DenominationKeySignature *dks
+      = &pkd->denom_sigs[i];
+
+    GNUNET_assert (0 ==
+                   json_array_append_new (
+                     denom_sigs,
+                     GNUNET_JSON_PACK (
+                       GNUNET_JSON_pack_data_auto ("h_denom_pub",
+                                                   &dks->h_denom_pub),
+                       GNUNET_JSON_pack_data_auto ("master_sig",
+                                                   &dks->master_sig))));
+  }
+  signkey_sigs = json_array ();
+  GNUNET_assert (NULL != signkey_sigs);
+  for (unsigned int i = 0; i<pkd->num_sign_sigs; i++)
+  {
+    const struct TALER_EXCHANGE_SigningKeySignature *sks
+      = &pkd->sign_sigs[i];
+
+    GNUNET_assert (0 ==
+                   json_array_append_new (
+                     signkey_sigs,
+                     GNUNET_JSON_PACK (
+                       GNUNET_JSON_pack_data_auto ("exchange_pub",
+                                                   &sks->exchange_pub),
+                       GNUNET_JSON_pack_data_auto ("master_sig",
+                                                   &sks->master_sig))));
+  }
+  body = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_array_steal ("denom_sigs",
+                                  denom_sigs),
+    GNUNET_JSON_pack_array_steal ("signkey_sigs",
+                                  signkey_sigs));
+  eh = TALER_EXCHANGE_curl_easy_get_ (ph->url);
+  if ( (NULL == eh) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&ph->post_ctx,
+                              eh,
+                              body)) )
+  {
+    GNUNET_break (0);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    json_decref (body);
+    GNUNET_free (ph->url);
+    return NULL;
+  }
+  json_decref (body);
+  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+              "Requesting URL '%s'\n",
+              ph->url);
+  ph->job = GNUNET_CURL_job_add2 (ctx,
+                                  eh,
+                                  ph->post_ctx.headers,
+                                  &handle_post_keys_finished,
+                                  ph);
+  if (NULL == ph->job)
+  {
+    TALER_EXCHANGE_post_management_keys_cancel (ph);
+    return NULL;
+  }
+  return ph;
+}
+
+
+void
+TALER_EXCHANGE_post_management_keys_cancel (
+  struct TALER_EXCHANGE_ManagementPostKeysHandle *ph)
+{
+  if (NULL != ph->job)
+  {
+    GNUNET_CURL_job_cancel (ph->job);
+    ph->job = NULL;
+  }
+  TALER_curl_easy_post_finished (&ph->post_ctx);
+  GNUNET_free (ph->url);
+  GNUNET_free (ph);
+}
diff --git a/src/lib/exchange_api_management_revoke_denomination_key.c 
b/src/lib/exchange_api_management_revoke_denomination_key.c
new file mode 100644
index 00000000..aa4d527a
--- /dev/null
+++ b/src/lib/exchange_api_management_revoke_denomination_key.c
@@ -0,0 +1,221 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2015-2020 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_management_revoke_denomination_key.c
+ * @brief functions to revoke an exchange denomination key
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "taler_json_lib.h"
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_exchange_service.h"
+#include "exchange_api_curl_defaults.h"
+#include "taler_signatures.h"
+#include "taler_curl_lib.h"
+#include "taler_json_lib.h"
+
+
+/**
+ * @brief Handle for a POST /management/denominations/$H_DENOM_PUB/revoke 
request.
+ */
+struct TALER_EXCHANGE_ManagementRevokeDenominationKeyHandle
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Minor context that holds body and headers.
+   */
+  struct TALER_CURL_PostContext post_ctx;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_ManagementRevokeDenominationKeyCallback cb;
+
+  /**
+   * Closure for @a cb.
+   */
+  void *cb_cls;
+
+  /**
+   * Reference to the execution context.
+   */
+  struct GNUNET_CURL_Context *ctx;
+};
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /management/denominations/$H_DENOM_PUB/revoke request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_ManagementRevokeDenominationKeyHandle 
*`
+ * @param response_code HTTP response code, 0 on error
+ * @param response response body, NULL if not in JSON
+ */
+static void
+handle_revoke_denomination_finished (void *cls,
+                                     long response_code,
+                                     const void *response)
+{
+  struct TALER_EXCHANGE_ManagementRevokeDenominationKeyHandle *rh = cls;
+  const json_t *json = response;
+  struct TALER_EXCHANGE_ManagementRevokeDenominationResponse rdr = {
+    .hr.http_status = (unsigned int) response_code,
+    .hr.reply = json
+  };
+
+  rh->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    /* no reply */
+    rdr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    rdr.hr.hint = "server offline?";
+    break;
+  case MHD_HTTP_NO_CONTENT:
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    rdr.hr.ec = TALER_JSON_get_error_code (json);
+    rdr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    rdr.hr.ec = TALER_JSON_get_error_code (json);
+    rdr.hr.hint = TALER_JSON_get_error_hint (json);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange management revoke 
denomination\n",
+                (unsigned int) response_code,
+                (int) rdr.hr.ec);
+    break;
+  }
+  if (NULL != rh->cb)
+  {
+    rh->cb (rh->cb_cls,
+            &rdr);
+    rh->cb = NULL;
+  }
+  TALER_EXCHANGE_management_revoke_denomination_key_cancel (rh);
+}
+
+
+struct TALER_EXCHANGE_ManagementRevokeDenominationKeyHandle *
+TALER_EXCHANGE_management_revoke_denomination_key (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  const struct TALER_DenominationHashP *h_denom_pub,
+  const struct TALER_MasterSignatureP *master_sig,
+  TALER_EXCHANGE_ManagementRevokeDenominationKeyCallback cb,
+  void *cb_cls)
+{
+  struct TALER_EXCHANGE_ManagementRevokeDenominationKeyHandle *rh;
+  CURL *eh;
+  json_t *body;
+
+  rh = GNUNET_new (struct 
TALER_EXCHANGE_ManagementRevokeDenominationKeyHandle);
+  rh->cb = cb;
+  rh->cb_cls = cb_cls;
+  rh->ctx = ctx;
+  {
+    char epub_str[sizeof (*h_denom_pub) * 2];
+    char arg_str[sizeof (epub_str) + 64];
+    char *end;
+
+    end = GNUNET_STRINGS_data_to_string (h_denom_pub,
+                                         sizeof (*h_denom_pub),
+                                         epub_str,
+                                         sizeof (epub_str));
+    *end = '\0';
+    GNUNET_snprintf (arg_str,
+                     sizeof (arg_str),
+                     "management/denominations/%s/revoke",
+                     epub_str);
+    rh->url = TALER_url_join (url,
+                              arg_str,
+                              NULL);
+  }
+  if (NULL == rh->url)
+  {
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Could not construct request URL.\n");
+    GNUNET_free (rh);
+    return NULL;
+  }
+  body = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_data_auto ("master_sig",
+                                master_sig));
+  if (NULL == body)
+  {
+    GNUNET_break (0);
+    GNUNET_free (rh->url);
+    GNUNET_free (rh);
+    return NULL;
+  }
+  eh = TALER_EXCHANGE_curl_easy_get_ (rh->url);
+  if ( (NULL == eh) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&rh->post_ctx,
+                              eh,
+                              body)) )
+  {
+    GNUNET_break (0);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    json_decref (body);
+    GNUNET_free (rh->url);
+    return NULL;
+  }
+  json_decref (body);
+  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+              "Requesting URL '%s'\n",
+              rh->url);
+  rh->job = GNUNET_CURL_job_add2 (ctx,
+                                  eh,
+                                  rh->post_ctx.headers,
+                                  &handle_revoke_denomination_finished,
+                                  rh);
+  if (NULL == rh->job)
+  {
+    TALER_EXCHANGE_management_revoke_denomination_key_cancel (rh);
+    return NULL;
+  }
+  return rh;
+}
+
+
+void
+TALER_EXCHANGE_management_revoke_denomination_key_cancel (
+  struct TALER_EXCHANGE_ManagementRevokeDenominationKeyHandle *rh)
+{
+  if (NULL != rh->job)
+  {
+    GNUNET_CURL_job_cancel (rh->job);
+    rh->job = NULL;
+  }
+  TALER_curl_easy_post_finished (&rh->post_ctx);
+  GNUNET_free (rh->url);
+  GNUNET_free (rh);
+}
diff --git a/src/lib/exchange_api_management_revoke_signing_key.c 
b/src/lib/exchange_api_management_revoke_signing_key.c
new file mode 100644
index 00000000..c4d63424
--- /dev/null
+++ b/src/lib/exchange_api_management_revoke_signing_key.c
@@ -0,0 +1,211 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2015-2021 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_management_revoke_signing_key.c
+ * @brief functions to revoke an exchange online signing key
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "taler_json_lib.h"
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_exchange_service.h"
+#include "exchange_api_curl_defaults.h"
+#include "taler_signatures.h"
+#include "taler_curl_lib.h"
+#include "taler_json_lib.h"
+
+
+struct TALER_EXCHANGE_ManagementRevokeSigningKeyHandle
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Minor context that holds body and headers.
+   */
+  struct TALER_CURL_PostContext post_ctx;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_ManagementRevokeSigningKeyCallback cb;
+
+  /**
+   * Closure for @a cb.
+   */
+  void *cb_cls;
+
+  /**
+   * Reference to the execution context.
+   */
+  struct GNUNET_CURL_Context *ctx;
+};
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /management/signkeys/%s/revoke request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_ManagementRevokeSigningKeyHandle *`
+ * @param response_code HTTP response code, 0 on error
+ * @param response response body, NULL if not in JSON
+ */
+static void
+handle_revoke_signing_finished (void *cls,
+                                long response_code,
+                                const void *response)
+{
+  struct TALER_EXCHANGE_ManagementRevokeSigningKeyHandle *rh = cls;
+  const json_t *json = response;
+  struct TALER_EXCHANGE_ManagementRevokeSigningKeyResponse rsr = {
+    .hr.http_status = (unsigned int) response_code,
+    .hr.reply = json
+  };
+
+  rh->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    /* no reply */
+    rsr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    rsr.hr.hint = "server offline?";
+    break;
+  case MHD_HTTP_NO_CONTENT:
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    rsr.hr.ec = TALER_JSON_get_error_code (json);
+    rsr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    rsr.hr.ec = TALER_JSON_get_error_code (json);
+    rsr.hr.hint = TALER_JSON_get_error_hint (json);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange management revoke 
signkey\n",
+                (unsigned int) response_code,
+                (int) rsr.hr.ec);
+    break;
+  }
+  if (NULL != rh->cb)
+  {
+    rh->cb (rh->cb_cls,
+            &rsr);
+    rh->cb = NULL;
+  }
+  TALER_EXCHANGE_management_revoke_signing_key_cancel (rh);
+}
+
+
+struct TALER_EXCHANGE_ManagementRevokeSigningKeyHandle *
+TALER_EXCHANGE_management_revoke_signing_key (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  const struct TALER_ExchangePublicKeyP *exchange_pub,
+  const struct TALER_MasterSignatureP *master_sig,
+  TALER_EXCHANGE_ManagementRevokeSigningKeyCallback cb,
+  void *cb_cls)
+{
+  struct TALER_EXCHANGE_ManagementRevokeSigningKeyHandle *rh;
+  CURL *eh;
+  json_t *body;
+
+  rh = GNUNET_new (struct TALER_EXCHANGE_ManagementRevokeSigningKeyHandle);
+  rh->cb = cb;
+  rh->cb_cls = cb_cls;
+  rh->ctx = ctx;
+  {
+    char epub_str[sizeof (*exchange_pub) * 2];
+    char arg_str[sizeof (epub_str) + 64];
+    char *end;
+
+    end = GNUNET_STRINGS_data_to_string (exchange_pub,
+                                         sizeof (*exchange_pub),
+                                         epub_str,
+                                         sizeof (epub_str));
+    *end = '\0';
+    GNUNET_snprintf (arg_str,
+                     sizeof (arg_str),
+                     "management/signkeys/%s/revoke",
+                     epub_str);
+    rh->url = TALER_url_join (url,
+                              arg_str,
+                              NULL);
+  }
+  if (NULL == rh->url)
+  {
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Could not construct request URL.\n");
+    GNUNET_free (rh);
+    return NULL;
+  }
+  body = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_data_auto ("master_sig",
+                                master_sig));
+  eh = TALER_EXCHANGE_curl_easy_get_ (rh->url);
+  if ( (NULL == eh) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&rh->post_ctx,
+                              eh,
+                              body)) )
+  {
+    GNUNET_break (0);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    json_decref (body);
+    GNUNET_free (rh->url);
+    return NULL;
+  }
+  json_decref (body);
+  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+              "Requesting URL '%s'\n",
+              rh->url);
+  rh->job = GNUNET_CURL_job_add2 (ctx,
+                                  eh,
+                                  rh->post_ctx.headers,
+                                  &handle_revoke_signing_finished,
+                                  rh);
+  if (NULL == rh->job)
+  {
+    TALER_EXCHANGE_management_revoke_signing_key_cancel (rh);
+    return NULL;
+  }
+  return rh;
+}
+
+
+void
+TALER_EXCHANGE_management_revoke_signing_key_cancel (
+  struct TALER_EXCHANGE_ManagementRevokeSigningKeyHandle *rh)
+{
+  if (NULL != rh->job)
+  {
+    GNUNET_CURL_job_cancel (rh->job);
+    rh->job = NULL;
+  }
+  TALER_curl_easy_post_finished (&rh->post_ctx);
+  GNUNET_free (rh->url);
+  GNUNET_free (rh);
+}
diff --git a/src/lib/exchange_api_management_set_global_fee.c 
b/src/lib/exchange_api_management_set_global_fee.c
new file mode 100644
index 00000000..54c37fd6
--- /dev/null
+++ b/src/lib/exchange_api_management_set_global_fee.c
@@ -0,0 +1,235 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2020-2022 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_management_set_global_fee.c
+ * @brief functions to set global fees at an exchange
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "taler_json_lib.h"
+#include <gnunet/gnunet_curl_lib.h>
+#include "exchange_api_curl_defaults.h"
+#include "taler_exchange_service.h"
+#include "taler_signatures.h"
+#include "taler_curl_lib.h"
+#include "taler_json_lib.h"
+
+
+struct TALER_EXCHANGE_ManagementSetGlobalFeeHandle
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Minor context that holds body and headers.
+   */
+  struct TALER_CURL_PostContext post_ctx;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_ManagementSetGlobalFeeCallback cb;
+
+  /**
+   * Closure for @a cb.
+   */
+  void *cb_cls;
+
+  /**
+   * Reference to the execution context.
+   */
+  struct GNUNET_CURL_Context *ctx;
+};
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /management/global request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_ManagementAuditorEnableHandle *`
+ * @param response_code HTTP response code, 0 on error
+ * @param response response body, NULL if not in JSON
+ */
+static void
+handle_set_global_fee_finished (void *cls,
+                                long response_code,
+                                const void *response)
+{
+  struct TALER_EXCHANGE_ManagementSetGlobalFeeHandle *sgfh = cls;
+  const json_t *json = response;
+  struct TALER_EXCHANGE_ManagementSetGlobalFeeResponse sfr = {
+    .hr.http_status = (unsigned int) response_code,
+    .hr.reply = json
+  };
+
+  sgfh->job = NULL;
+  switch (response_code)
+  {
+  case MHD_HTTP_NO_CONTENT:
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    sfr.hr.ec = TALER_JSON_get_error_code (json);
+    sfr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Server did not find handler at `%s'. Did you configure the 
correct exchange base URL?\n",
+                sgfh->url);
+    if (NULL != json)
+    {
+      sfr.hr.ec = TALER_JSON_get_error_code (json);
+      sfr.hr.hint = TALER_JSON_get_error_hint (json);
+    }
+    else
+    {
+      sfr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+      sfr.hr.hint = TALER_ErrorCode_get_hint (sfr.hr.ec);
+    }
+    break;
+  case MHD_HTTP_CONFLICT:
+    sfr.hr.ec = TALER_JSON_get_error_code (json);
+    sfr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_PRECONDITION_FAILED:
+    sfr.hr.ec = TALER_JSON_get_error_code (json);
+    sfr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    sfr.hr.ec = TALER_JSON_get_error_code (json);
+    sfr.hr.hint = TALER_JSON_get_error_hint (json);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange management set 
global fee\n",
+                (unsigned int) response_code,
+                (int) sfr.hr.ec);
+    break;
+  }
+  if (NULL != sgfh->cb)
+  {
+    sgfh->cb (sgfh->cb_cls,
+              &sfr);
+    sgfh->cb = NULL;
+  }
+  TALER_EXCHANGE_management_set_global_fees_cancel (sgfh);
+}
+
+
+struct TALER_EXCHANGE_ManagementSetGlobalFeeHandle *
+TALER_EXCHANGE_management_set_global_fees (
+  struct GNUNET_CURL_Context *ctx,
+  const char *exchange_base_url,
+  struct GNUNET_TIME_Timestamp validity_start,
+  struct GNUNET_TIME_Timestamp validity_end,
+  const struct TALER_GlobalFeeSet *fees,
+  struct GNUNET_TIME_Relative purse_timeout,
+  struct GNUNET_TIME_Relative history_expiration,
+  uint32_t purse_account_limit,
+  const struct TALER_MasterSignatureP *master_sig,
+  TALER_EXCHANGE_ManagementSetGlobalFeeCallback cb,
+  void *cb_cls)
+{
+  struct TALER_EXCHANGE_ManagementSetGlobalFeeHandle *sgfh;
+  CURL *eh;
+  json_t *body;
+
+  sgfh = GNUNET_new (struct TALER_EXCHANGE_ManagementSetGlobalFeeHandle);
+  sgfh->cb = cb;
+  sgfh->cb_cls = cb_cls;
+  sgfh->ctx = ctx;
+  sgfh->url = TALER_url_join (exchange_base_url,
+                              "management/global-fee",
+                              NULL);
+  if (NULL == sgfh->url)
+  {
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Could not construct request URL.\n");
+    GNUNET_free (sgfh);
+    return NULL;
+  }
+  body = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_data_auto ("master_sig",
+                                master_sig),
+    GNUNET_JSON_pack_timestamp ("fee_start",
+                                validity_start),
+    GNUNET_JSON_pack_timestamp ("fee_end",
+                                validity_end),
+    TALER_JSON_pack_amount ("history_fee",
+                            &fees->history),
+    TALER_JSON_pack_amount ("account_fee",
+                            &fees->account),
+    TALER_JSON_pack_amount ("purse_fee",
+                            &fees->purse),
+    GNUNET_JSON_pack_time_rel ("purse_timeout",
+                               purse_timeout),
+    GNUNET_JSON_pack_time_rel ("history_expiration",
+                               history_expiration),
+    GNUNET_JSON_pack_uint64 ("purse_account_limit",
+                             purse_account_limit));
+  eh = TALER_EXCHANGE_curl_easy_get_ (sgfh->url);
+  if ( (NULL == eh) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&sgfh->post_ctx,
+                              eh,
+                              body)) )
+  {
+    GNUNET_break (0);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    json_decref (body);
+    GNUNET_free (sgfh->url);
+    return NULL;
+  }
+  json_decref (body);
+  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+              "Requesting URL '%s'\n",
+              sgfh->url);
+  sgfh->job = GNUNET_CURL_job_add2 (ctx,
+                                    eh,
+                                    sgfh->post_ctx.headers,
+                                    &handle_set_global_fee_finished,
+                                    sgfh);
+  if (NULL == sgfh->job)
+  {
+    TALER_EXCHANGE_management_set_global_fees_cancel (sgfh);
+    return NULL;
+  }
+  return sgfh;
+}
+
+
+void
+TALER_EXCHANGE_management_set_global_fees_cancel (
+  struct TALER_EXCHANGE_ManagementSetGlobalFeeHandle *sgfh)
+{
+  if (NULL != sgfh->job)
+  {
+    GNUNET_CURL_job_cancel (sgfh->job);
+    sgfh->job = NULL;
+  }
+  TALER_curl_easy_post_finished (&sgfh->post_ctx);
+  GNUNET_free (sgfh->url);
+  GNUNET_free (sgfh);
+}
diff --git a/src/lib/exchange_api_management_set_wire_fee.c 
b/src/lib/exchange_api_management_set_wire_fee.c
new file mode 100644
index 00000000..03cab8c9
--- /dev/null
+++ b/src/lib/exchange_api_management_set_wire_fee.c
@@ -0,0 +1,227 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2020-2022 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_management_set_wire_fee.c
+ * @brief functions to set wire fees at an exchange
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "taler_json_lib.h"
+#include <gnunet/gnunet_curl_lib.h>
+#include "exchange_api_curl_defaults.h"
+#include "taler_exchange_service.h"
+#include "taler_signatures.h"
+#include "taler_curl_lib.h"
+#include "taler_json_lib.h"
+
+
+struct TALER_EXCHANGE_ManagementSetWireFeeHandle
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Minor context that holds body and headers.
+   */
+  struct TALER_CURL_PostContext post_ctx;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_ManagementSetWireFeeCallback cb;
+
+  /**
+   * Closure for @a cb.
+   */
+  void *cb_cls;
+
+  /**
+   * Reference to the execution context.
+   */
+  struct GNUNET_CURL_Context *ctx;
+};
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /management/wire request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_ManagementAuditorEnableHandle *`
+ * @param response_code HTTP response code, 0 on error
+ * @param response response body, NULL if not in JSON
+ */
+static void
+handle_set_wire_fee_finished (void *cls,
+                              long response_code,
+                              const void *response)
+{
+  struct TALER_EXCHANGE_ManagementSetWireFeeHandle *swfh = cls;
+  const json_t *json = response;
+  struct TALER_EXCHANGE_ManagementSetWireFeeResponse swr = {
+    .hr.http_status = (unsigned int) response_code,
+    .hr.reply = json
+  };
+
+  swfh->job = NULL;
+  switch (response_code)
+  {
+  case MHD_HTTP_NO_CONTENT:
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    swr.hr.ec = TALER_JSON_get_error_code (json);
+    swr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Server did not find handler at `%s'. Did you configure the 
correct exchange base URL?\n",
+                swfh->url);
+    if (NULL != json)
+    {
+      swr.hr.ec = TALER_JSON_get_error_code (json);
+      swr.hr.hint = TALER_JSON_get_error_hint (json);
+    }
+    else
+    {
+      swr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+      swr.hr.hint = TALER_ErrorCode_get_hint (swr.hr.ec);
+    }
+    break;
+  case MHD_HTTP_CONFLICT:
+    swr.hr.ec = TALER_JSON_get_error_code (json);
+    swr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_PRECONDITION_FAILED:
+    swr.hr.ec = TALER_JSON_get_error_code (json);
+    swr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    swr.hr.ec = TALER_JSON_get_error_code (json);
+    swr.hr.hint = TALER_JSON_get_error_hint (json);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange management set 
wire fee\n",
+                (unsigned int) response_code,
+                (int) swr.hr.ec);
+    break;
+  }
+  if (NULL != swfh->cb)
+  {
+    swfh->cb (swfh->cb_cls,
+              &swr);
+    swfh->cb = NULL;
+  }
+  TALER_EXCHANGE_management_set_wire_fees_cancel (swfh);
+}
+
+
+struct TALER_EXCHANGE_ManagementSetWireFeeHandle *
+TALER_EXCHANGE_management_set_wire_fees (
+  struct GNUNET_CURL_Context *ctx,
+  const char *exchange_base_url,
+  const char *wire_method,
+  struct GNUNET_TIME_Timestamp validity_start,
+  struct GNUNET_TIME_Timestamp validity_end,
+  const struct TALER_WireFeeSet *fees,
+  const struct TALER_MasterSignatureP *master_sig,
+  TALER_EXCHANGE_ManagementSetWireFeeCallback cb,
+  void *cb_cls)
+{
+  struct TALER_EXCHANGE_ManagementSetWireFeeHandle *swfh;
+  CURL *eh;
+  json_t *body;
+
+  swfh = GNUNET_new (struct TALER_EXCHANGE_ManagementSetWireFeeHandle);
+  swfh->cb = cb;
+  swfh->cb_cls = cb_cls;
+  swfh->ctx = ctx;
+  swfh->url = TALER_url_join (exchange_base_url,
+                              "management/wire-fee",
+                              NULL);
+  if (NULL == swfh->url)
+  {
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Could not construct request URL.\n");
+    GNUNET_free (swfh);
+    return NULL;
+  }
+  body = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_string ("wire_method",
+                             wire_method),
+    GNUNET_JSON_pack_data_auto ("master_sig",
+                                master_sig),
+    GNUNET_JSON_pack_timestamp ("fee_start",
+                                validity_start),
+    GNUNET_JSON_pack_timestamp ("fee_end",
+                                validity_end),
+    TALER_JSON_pack_amount ("closing_fee",
+                            &fees->closing),
+    TALER_JSON_pack_amount ("wire_fee",
+                            &fees->wire));
+  eh = TALER_EXCHANGE_curl_easy_get_ (swfh->url);
+  if ( (NULL == eh) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&swfh->post_ctx,
+                              eh,
+                              body)) )
+  {
+    GNUNET_break (0);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    json_decref (body);
+    GNUNET_free (swfh->url);
+    return NULL;
+  }
+  json_decref (body);
+  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+              "Requesting URL '%s'\n",
+              swfh->url);
+  swfh->job = GNUNET_CURL_job_add2 (ctx,
+                                    eh,
+                                    swfh->post_ctx.headers,
+                                    &handle_set_wire_fee_finished,
+                                    swfh);
+  if (NULL == swfh->job)
+  {
+    TALER_EXCHANGE_management_set_wire_fees_cancel (swfh);
+    return NULL;
+  }
+  return swfh;
+}
+
+
+void
+TALER_EXCHANGE_management_set_wire_fees_cancel (
+  struct TALER_EXCHANGE_ManagementSetWireFeeHandle *swfh)
+{
+  if (NULL != swfh->job)
+  {
+    GNUNET_CURL_job_cancel (swfh->job);
+    swfh->job = NULL;
+  }
+  TALER_curl_easy_post_finished (&swfh->post_ctx);
+  GNUNET_free (swfh->url);
+  GNUNET_free (swfh);
+}
diff --git a/src/lib/exchange_api_management_update_aml_officer.c 
b/src/lib/exchange_api_management_update_aml_officer.c
new file mode 100644
index 00000000..76cbc7e8
--- /dev/null
+++ b/src/lib/exchange_api_management_update_aml_officer.c
@@ -0,0 +1,229 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2023 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_management_update_aml_officer.c
+ * @brief functions to update AML officer status
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "taler_json_lib.h"
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_exchange_service.h"
+#include "exchange_api_curl_defaults.h"
+#include "taler_signatures.h"
+#include "taler_curl_lib.h"
+#include "taler_json_lib.h"
+
+
+struct TALER_EXCHANGE_ManagementUpdateAmlOfficer
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Minor context that holds body and headers.
+   */
+  struct TALER_CURL_PostContext post_ctx;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_ManagementUpdateAmlOfficerCallback cb;
+
+  /**
+   * Closure for @a cb.
+   */
+  void *cb_cls;
+
+  /**
+   * Reference to the execution context.
+   */
+  struct GNUNET_CURL_Context *ctx;
+};
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /management/wire request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_ManagementAuditorEnableHandle *`
+ * @param response_code HTTP response code, 0 on error
+ * @param response response body, NULL if not in JSON
+ */
+static void
+handle_update_aml_officer_finished (void *cls,
+                                    long response_code,
+                                    const void *response)
+{
+  struct TALER_EXCHANGE_ManagementUpdateAmlOfficer *wh = cls;
+  const json_t *json = response;
+  struct TALER_EXCHANGE_ManagementUpdateAmlOfficerResponse uar = {
+    .hr.http_status = (unsigned int) response_code,
+    .hr.reply = json
+  };
+
+  wh->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    /* no reply */
+    uar.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    uar.hr.hint = "server offline?";
+    break;
+  case MHD_HTTP_NO_CONTENT:
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    uar.hr.ec = TALER_JSON_get_error_code (json);
+    uar.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Server did not find handler at `%s'. Did you configure the 
correct exchange base URL?\n",
+                wh->url);
+    if (NULL != json)
+    {
+      uar.hr.ec = TALER_JSON_get_error_code (json);
+      uar.hr.hint = TALER_JSON_get_error_hint (json);
+    }
+    else
+    {
+      uar.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+      uar.hr.hint = TALER_ErrorCode_get_hint (uar.hr.ec);
+    }
+    break;
+  case MHD_HTTP_CONFLICT:
+    uar.hr.ec = TALER_JSON_get_error_code (json);
+    uar.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    uar.hr.ec = TALER_JSON_get_error_code (json);
+    uar.hr.hint = TALER_JSON_get_error_hint (json);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange management update 
AML officer\n",
+                (unsigned int) response_code,
+                (int) uar.hr.ec);
+    break;
+  }
+  if (NULL != wh->cb)
+  {
+    wh->cb (wh->cb_cls,
+            &uar);
+    wh->cb = NULL;
+  }
+  TALER_EXCHANGE_management_update_aml_officer_cancel (wh);
+}
+
+
+struct TALER_EXCHANGE_ManagementUpdateAmlOfficer *
+TALER_EXCHANGE_management_update_aml_officer (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  const struct TALER_AmlOfficerPublicKeyP *officer_pub,
+  const char *officer_name,
+  struct GNUNET_TIME_Timestamp change_date,
+  bool is_active,
+  bool read_only,
+  const struct TALER_MasterSignatureP *master_sig,
+  TALER_EXCHANGE_ManagementUpdateAmlOfficerCallback cb,
+  void *cb_cls)
+{
+  struct TALER_EXCHANGE_ManagementUpdateAmlOfficer *wh;
+  CURL *eh;
+  json_t *body;
+
+  wh = GNUNET_new (struct TALER_EXCHANGE_ManagementUpdateAmlOfficer);
+  wh->cb = cb;
+  wh->cb_cls = cb_cls;
+  wh->ctx = ctx;
+  wh->url = TALER_url_join (url,
+                            "management/aml-officers",
+                            NULL);
+  if (NULL == wh->url)
+  {
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Could not construct request URL.\n");
+    GNUNET_free (wh);
+    return NULL;
+  }
+  body = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_string ("officer_name",
+                             officer_name),
+    GNUNET_JSON_pack_data_auto ("officer_pub",
+                                officer_pub),
+    GNUNET_JSON_pack_data_auto ("master_sig",
+                                master_sig),
+    GNUNET_JSON_pack_bool ("is_active",
+                           is_active),
+    GNUNET_JSON_pack_bool ("read_only",
+                           read_only),
+    GNUNET_JSON_pack_timestamp ("change_date",
+                                change_date));
+  eh = TALER_EXCHANGE_curl_easy_get_ (wh->url);
+  if ( (NULL == eh) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&wh->post_ctx,
+                              eh,
+                              body)) )
+  {
+    GNUNET_break (0);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    json_decref (body);
+    GNUNET_free (wh->url);
+    return NULL;
+  }
+  json_decref (body);
+  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+              "Requesting URL '%s'\n",
+              wh->url);
+  wh->job = GNUNET_CURL_job_add2 (ctx,
+                                  eh,
+                                  wh->post_ctx.headers,
+                                  &handle_update_aml_officer_finished,
+                                  wh);
+  if (NULL == wh->job)
+  {
+    TALER_EXCHANGE_management_update_aml_officer_cancel (wh);
+    return NULL;
+  }
+  return wh;
+}
+
+
+void
+TALER_EXCHANGE_management_update_aml_officer_cancel (
+  struct TALER_EXCHANGE_ManagementUpdateAmlOfficer *wh)
+{
+  if (NULL != wh->job)
+  {
+    GNUNET_CURL_job_cancel (wh->job);
+    wh->job = NULL;
+  }
+  TALER_curl_easy_post_finished (&wh->post_ctx);
+  GNUNET_free (wh->url);
+  GNUNET_free (wh);
+}
diff --git a/src/lib/exchange_api_management_wire_disable.c 
b/src/lib/exchange_api_management_wire_disable.c
new file mode 100644
index 00000000..30749b0e
--- /dev/null
+++ b/src/lib/exchange_api_management_wire_disable.c
@@ -0,0 +1,220 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2015-2023 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_management_wire_disable.c
+ * @brief functions to disable an exchange wire method / bank account
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "taler_json_lib.h"
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_exchange_service.h"
+#include "exchange_api_curl_defaults.h"
+#include "taler_signatures.h"
+#include "taler_curl_lib.h"
+#include "taler_json_lib.h"
+
+
+struct TALER_EXCHANGE_ManagementWireDisableHandle
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Minor context that holds body and headers.
+   */
+  struct TALER_CURL_PostContext post_ctx;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_ManagementWireDisableCallback cb;
+
+  /**
+   * Closure for @a cb.
+   */
+  void *cb_cls;
+
+  /**
+   * Reference to the execution context.
+   */
+  struct GNUNET_CURL_Context *ctx;
+};
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /management/wire/disable request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_ManagementAuditorDisableHandle *`
+ * @param response_code HTTP response code, 0 on error
+ * @param response response body, NULL if not in JSON
+ */
+static void
+handle_auditor_disable_finished (void *cls,
+                                 long response_code,
+                                 const void *response)
+{
+  struct TALER_EXCHANGE_ManagementWireDisableHandle *wh = cls;
+  const json_t *json = response;
+  struct TALER_EXCHANGE_ManagementWireDisableResponse wdr = {
+    .hr.http_status = (unsigned int) response_code,
+    .hr.reply = json
+  };
+
+  wh->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    /* no reply */
+    wdr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    wdr.hr.hint = "server offline?";
+    break;
+  case MHD_HTTP_NO_CONTENT:
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    wdr.hr.ec = TALER_JSON_get_error_code (json);
+    wdr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Server did not find handler at `%s'. Did you configure the 
correct exchange base URL?\n",
+                wh->url);
+    if (NULL != json)
+    {
+      wdr.hr.ec = TALER_JSON_get_error_code (json);
+      wdr.hr.hint = TALER_JSON_get_error_hint (json);
+    }
+    else
+    {
+      wdr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+      wdr.hr.hint = TALER_ErrorCode_get_hint (wdr.hr.ec);
+    }
+    break;
+  case MHD_HTTP_CONFLICT:
+    wdr.hr.ec = TALER_JSON_get_error_code (json);
+    wdr.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    wdr.hr.ec = TALER_JSON_get_error_code (json);
+    wdr.hr.hint = TALER_JSON_get_error_hint (json);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d exchange management disable 
wire\n",
+                (unsigned int) response_code,
+                (int) wdr.hr.ec);
+    break;
+  }
+  if (NULL != wh->cb)
+  {
+    wh->cb (wh->cb_cls,
+            &wdr);
+    wh->cb = NULL;
+  }
+  TALER_EXCHANGE_management_disable_wire_cancel (wh);
+}
+
+
+struct TALER_EXCHANGE_ManagementWireDisableHandle *
+TALER_EXCHANGE_management_disable_wire (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  const char *payto_uri,
+  struct GNUNET_TIME_Timestamp validity_end,
+  const struct TALER_MasterSignatureP *master_sig,
+  TALER_EXCHANGE_ManagementWireDisableCallback cb,
+  void *cb_cls)
+{
+  struct TALER_EXCHANGE_ManagementWireDisableHandle *wh;
+  CURL *eh;
+  json_t *body;
+
+  wh = GNUNET_new (struct TALER_EXCHANGE_ManagementWireDisableHandle);
+  wh->cb = cb;
+  wh->cb_cls = cb_cls;
+  wh->ctx = ctx;
+  wh->url = TALER_url_join (url,
+                            "management/wire/disable",
+                            NULL);
+  if (NULL == wh->url)
+  {
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Could not construct request URL.\n");
+    GNUNET_free (wh);
+    return NULL;
+  }
+  body = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_string ("payto_uri",
+                             payto_uri),
+    GNUNET_JSON_pack_data_auto ("master_sig_del",
+                                master_sig),
+    GNUNET_JSON_pack_timestamp ("validity_end",
+                                validity_end));
+  eh = TALER_EXCHANGE_curl_easy_get_ (wh->url);
+  if ( (NULL == eh) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&wh->post_ctx,
+                              eh,
+                              body)) )
+  {
+    GNUNET_break (0);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    json_decref (body);
+    GNUNET_free (wh->url);
+    return NULL;
+  }
+  json_decref (body);
+  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+              "Requesting URL '%s'\n",
+              wh->url);
+  wh->job = GNUNET_CURL_job_add2 (ctx,
+                                  eh,
+                                  wh->post_ctx.headers,
+                                  &handle_auditor_disable_finished,
+                                  wh);
+  if (NULL == wh->job)
+  {
+    TALER_EXCHANGE_management_disable_wire_cancel (wh);
+    return NULL;
+  }
+  return wh;
+}
+
+
+void
+TALER_EXCHANGE_management_disable_wire_cancel (
+  struct TALER_EXCHANGE_ManagementWireDisableHandle *wh)
+{
+  if (NULL != wh->job)
+  {
+    GNUNET_CURL_job_cancel (wh->job);
+    wh->job = NULL;
+  }
+  TALER_curl_easy_post_finished (&wh->post_ctx);
+  GNUNET_free (wh->url);
+  GNUNET_free (wh);
+}
diff --git a/src/lib/exchange_api_management_wire_enable.c 
b/src/lib/exchange_api_management_wire_enable.c
new file mode 100644
index 00000000..5add3e0b
--- /dev/null
+++ b/src/lib/exchange_api_management_wire_enable.c
@@ -0,0 +1,245 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2015-2023 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_management_wire_enable.c
+ * @brief functions to enable an exchange wire method / bank account
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "taler_json_lib.h"
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_exchange_service.h"
+#include "exchange_api_curl_defaults.h"
+#include "taler_signatures.h"
+#include "taler_curl_lib.h"
+#include "taler_json_lib.h"
+
+
+struct TALER_EXCHANGE_ManagementWireEnableHandle
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Minor context that holds body and headers.
+   */
+  struct TALER_CURL_PostContext post_ctx;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_ManagementWireEnableCallback cb;
+
+  /**
+   * Closure for @a cb.
+   */
+  void *cb_cls;
+
+  /**
+   * Reference to the execution context.
+   */
+  struct GNUNET_CURL_Context *ctx;
+};
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /management/wire request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_ManagementAuditorEnableHandle *`
+ * @param response_code HTTP response code, 0 on error
+ * @param response response body, NULL if not in JSON
+ */
+static void
+handle_auditor_enable_finished (void *cls,
+                                long response_code,
+                                const void *response)
+{
+  struct TALER_EXCHANGE_ManagementWireEnableHandle *wh = cls;
+  const json_t *json = response;
+  struct TALER_EXCHANGE_ManagementWireEnableResponse wer = {
+    .hr.http_status = (unsigned int) response_code,
+    .hr.reply = json
+  };
+
+  wh->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    /* no reply */
+    wer.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    wer.hr.hint = "server offline?";
+    break;
+  case MHD_HTTP_NO_CONTENT:
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    wer.hr.ec = TALER_JSON_get_error_code (json);
+    wer.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Server did not find handler at `%s'. Did you configure the 
correct exchange base URL?\n",
+                wh->url);
+    if (NULL != json)
+    {
+      wer.hr.ec = TALER_JSON_get_error_code (json);
+      wer.hr.hint = TALER_JSON_get_error_hint (json);
+    }
+    else
+    {
+      wer.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+      wer.hr.hint = TALER_ErrorCode_get_hint (wer.hr.ec);
+    }
+    break;
+  case MHD_HTTP_CONFLICT:
+    wer.hr.ec = TALER_JSON_get_error_code (json);
+    wer.hr.hint = TALER_JSON_get_error_hint (json);
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    wer.hr.ec = TALER_JSON_get_error_code (json);
+    wer.hr.hint = TALER_JSON_get_error_hint (json);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange management enable 
wire\n",
+                (unsigned int) response_code,
+                (int) wer.hr.ec);
+    break;
+  }
+  if (NULL != wh->cb)
+  {
+    wh->cb (wh->cb_cls,
+            &wer);
+    wh->cb = NULL;
+  }
+  TALER_EXCHANGE_management_enable_wire_cancel (wh);
+}
+
+
+struct TALER_EXCHANGE_ManagementWireEnableHandle *
+TALER_EXCHANGE_management_enable_wire (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  const char *payto_uri,
+  const char *conversion_url,
+  const json_t *debit_restrictions,
+  const json_t *credit_restrictions,
+  struct GNUNET_TIME_Timestamp validity_start,
+  const struct TALER_MasterSignatureP *master_sig1,
+  const struct TALER_MasterSignatureP *master_sig2,
+  TALER_EXCHANGE_ManagementWireEnableCallback cb,
+  void *cb_cls)
+{
+  struct TALER_EXCHANGE_ManagementWireEnableHandle *wh;
+  CURL *eh;
+  json_t *body;
+
+  {
+    char *msg = TALER_payto_validate (payto_uri);
+
+    if (NULL != msg)
+    {
+      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                  "payto URI is malformed: %s\n",
+                  msg);
+      GNUNET_free (msg);
+      return NULL;
+    }
+  }
+  wh = GNUNET_new (struct TALER_EXCHANGE_ManagementWireEnableHandle);
+  wh->cb = cb;
+  wh->cb_cls = cb_cls;
+  wh->ctx = ctx;
+  wh->url = TALER_url_join (url,
+                            "management/wire",
+                            NULL);
+  if (NULL == wh->url)
+  {
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Could not construct request URL.\n");
+    GNUNET_free (wh);
+    return NULL;
+  }
+  body = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_string ("payto_uri",
+                             payto_uri),
+    GNUNET_JSON_pack_array_incref ("debit_restrictions",
+                                   (json_t *) debit_restrictions),
+    GNUNET_JSON_pack_array_incref ("credit_restrictions",
+                                   (json_t *) credit_restrictions),
+    GNUNET_JSON_pack_allow_null (
+      GNUNET_JSON_pack_string ("conversion_url",
+                               conversion_url)),
+    GNUNET_JSON_pack_data_auto ("master_sig_add",
+                                master_sig1),
+    GNUNET_JSON_pack_data_auto ("master_sig_wire",
+                                master_sig2),
+    GNUNET_JSON_pack_timestamp ("validity_start",
+                                validity_start));
+  eh = TALER_EXCHANGE_curl_easy_get_ (wh->url);
+  if ( (NULL == eh) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&wh->post_ctx,
+                              eh,
+                              body)) )
+  {
+    GNUNET_break (0);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    json_decref (body);
+    GNUNET_free (wh->url);
+    return NULL;
+  }
+  json_decref (body);
+  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+              "Requesting URL '%s'\n",
+              wh->url);
+  wh->job = GNUNET_CURL_job_add2 (ctx,
+                                  eh,
+                                  wh->post_ctx.headers,
+                                  &handle_auditor_enable_finished,
+                                  wh);
+  if (NULL == wh->job)
+  {
+    TALER_EXCHANGE_management_enable_wire_cancel (wh);
+    return NULL;
+  }
+  return wh;
+}
+
+
+void
+TALER_EXCHANGE_management_enable_wire_cancel (
+  struct TALER_EXCHANGE_ManagementWireEnableHandle *wh)
+{
+  if (NULL != wh->job)
+  {
+    GNUNET_CURL_job_cancel (wh->job);
+    wh->job = NULL;
+  }
+  TALER_curl_easy_post_finished (&wh->post_ctx);
+  GNUNET_free (wh->url);
+  GNUNET_free (wh);
+}
diff --git a/src/lib/exchange_api_melt.c b/src/lib/exchange_api_melt.c
new file mode 100644
index 00000000..ba4241da
--- /dev/null
+++ b/src/lib/exchange_api_melt.c
@@ -0,0 +1,596 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2015-2023 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_melt.c
+ * @brief Implementation of the /coins/$COIN_PUB/melt request
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <jansson.h>
+#include <microhttpd.h> /* just for HTTP status codes */
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_json_lib.h"
+#include "taler_exchange_service.h"
+#include "exchange_api_common.h"
+#include "exchange_api_handle.h"
+#include "taler_signatures.h"
+#include "exchange_api_curl_defaults.h"
+#include "exchange_api_refresh_common.h"
+
+
+/**
+ * @brief A /coins/$COIN_PUB/melt Handle
+ */
+struct TALER_EXCHANGE_MeltHandle
+{
+
+  /**
+   * The keys of the this request handle will use
+   */
+  struct TALER_EXCHANGE_Keys *keys;
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * The exchange base url.
+   */
+  char *exchange_url;
+
+  /**
+   * Curl context.
+   */
+  struct GNUNET_CURL_Context *cctx;
+
+  /**
+   * Context for #TEH_curl_easy_post(). Keeps the data that must
+   * persist for Curl to make the upload.
+   */
+  struct TALER_CURL_PostContext ctx;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with refresh melt failure results.
+   */
+  TALER_EXCHANGE_MeltCallback melt_cb;
+
+  /**
+   * Closure for @e result_cb and @e melt_failure_cb.
+   */
+  void *melt_cb_cls;
+
+  /**
+   * Actual information about the melt operation.
+   */
+  struct MeltData md;
+
+  /**
+   * The secret the entire melt operation is seeded from.
+   */
+  struct TALER_RefreshMasterSecretP rms;
+
+  /**
+   * Details about the characteristics of the requested melt operation.
+   */
+  const struct TALER_EXCHANGE_RefreshData *rd;
+
+  /**
+   * Array of `num_fresh_coins` per-coin values
+   * returned from melt operation.
+   */
+  struct TALER_EXCHANGE_MeltBlindingDetail *mbds;
+
+  /**
+   * Handle for the preflight request, or NULL.
+   */
+  struct TALER_EXCHANGE_CsRMeltHandle *csr;
+
+  /**
+   * Public key of the coin being melted.
+   */
+  struct TALER_CoinSpendPublicKeyP coin_pub;
+
+  /**
+   * Signature affirming the melt.
+   */
+  struct TALER_CoinSpendSignatureP coin_sig;
+
+  /**
+   * @brief Public information about the coin's denomination key
+   */
+  const struct TALER_EXCHANGE_DenomPublicKey *dki;
+
+  /**
+   * Gamma value chosen by the exchange during melt.
+   */
+  uint32_t noreveal_index;
+
+  /**
+   * True if we need to include @e rms in our melt request.
+   */
+  bool send_rms;
+};
+
+
+/**
+ * Verify that the signature on the "200 OK" response
+ * from the exchange is valid.
+ *
+ * @param[in,out] mh melt handle
+ * @param json json reply with the signature
+ * @param[out] exchange_pub public key of the exchange used for the signature
+ * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR if not
+ */
+static enum GNUNET_GenericReturnValue
+verify_melt_signature_ok (struct TALER_EXCHANGE_MeltHandle *mh,
+                          const json_t *json,
+                          struct TALER_ExchangePublicKeyP *exchange_pub)
+{
+  struct TALER_ExchangeSignatureP exchange_sig;
+  const struct TALER_EXCHANGE_Keys *key_state;
+  struct GNUNET_JSON_Specification spec[] = {
+    GNUNET_JSON_spec_fixed_auto ("exchange_sig",
+                                 &exchange_sig),
+    GNUNET_JSON_spec_fixed_auto ("exchange_pub",
+                                 exchange_pub),
+    GNUNET_JSON_spec_uint32 ("noreveal_index",
+                             &mh->noreveal_index),
+    GNUNET_JSON_spec_end ()
+  };
+
+  if (GNUNET_OK !=
+      GNUNET_JSON_parse (json,
+                         spec,
+                         NULL, NULL))
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+  /* check that exchange signing key is permitted */
+  key_state = mh->keys;
+  if (GNUNET_OK !=
+      TALER_EXCHANGE_test_signing_key (key_state,
+                                       exchange_pub))
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+
+  /* check that noreveal index is in permitted range */
+  if (TALER_CNC_KAPPA <= mh->noreveal_index)
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+
+  if (GNUNET_OK !=
+      TALER_exchange_online_melt_confirmation_verify (
+        &mh->md.rc,
+        mh->noreveal_index,
+        exchange_pub,
+        &exchange_sig))
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+  return GNUNET_OK;
+}
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /coins/$COIN_PUB/melt request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_MeltHandle`
+ * @param response_code HTTP response code, 0 on error
+ * @param response parsed JSON result, NULL on error
+ */
+static void
+handle_melt_finished (void *cls,
+                      long response_code,
+                      const void *response)
+{
+  struct TALER_EXCHANGE_MeltHandle *mh = cls;
+  const json_t *j = response;
+  struct TALER_EXCHANGE_MeltResponse mr = {
+    .hr.reply = j,
+    .hr.http_status = (unsigned int) response_code
+  };
+
+  mh->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    mr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    break;
+  case MHD_HTTP_OK:
+    if (GNUNET_OK !=
+        verify_melt_signature_ok (mh,
+                                  j,
+                                  &mr.details.ok.sign_key))
+    {
+      GNUNET_break_op (0);
+      mr.hr.http_status = 0;
+      mr.hr.ec = TALER_EC_EXCHANGE_MELT_INVALID_SIGNATURE_BY_EXCHANGE;
+      break;
+    }
+    mr.details.ok.noreveal_index = mh->noreveal_index;
+    mr.details.ok.num_mbds = mh->rd->fresh_pks_len;
+    mr.details.ok.mbds = mh->mbds;
+    mh->melt_cb (mh->melt_cb_cls,
+                 &mr);
+    mh->melt_cb = NULL;
+    break;
+  case MHD_HTTP_BAD_REQUEST:
+    /* This should never happen, either us or the exchange is buggy
+       (or API version conflict); just pass JSON reply to the application */
+    mr.hr.ec = TALER_JSON_get_error_code (j);
+    mr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_CONFLICT:
+    mr.hr.ec = TALER_JSON_get_error_code (j);
+    mr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    /* Nothing really to verify, exchange says one of the signatures is
+       invalid; assuming we checked them, this should never happen, we
+       should pass the JSON reply to the application */
+    mr.hr.ec = TALER_JSON_get_error_code (j);
+    mr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    /* Nothing really to verify, this should never
+       happen, we should pass the JSON reply to the application */
+    mr.hr.ec = TALER_JSON_get_error_code (j);
+    mr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_INTERNAL_SERVER_ERROR:
+    /* Server had an internal issue; we should retry, but this API
+       leaves this to the application */
+    mr.hr.ec = TALER_JSON_get_error_code (j);
+    mr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  default:
+    /* unexpected response code */
+    mr.hr.ec = TALER_JSON_get_error_code (j);
+    mr.hr.hint = TALER_JSON_get_error_hint (j);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange melt\n",
+                (unsigned int) response_code,
+                mr.hr.ec);
+    GNUNET_break_op (0);
+    break;
+  }
+  if (NULL != mh->melt_cb)
+    mh->melt_cb (mh->melt_cb_cls,
+                 &mr);
+  TALER_EXCHANGE_melt_cancel (mh);
+}
+
+
+/**
+ * Start the actual melt operation, now that we have
+ * the exchange's input values.
+ *
+ * @param[in,out] mh melt operation to run
+ * @return #GNUNET_OK if we could start the operation
+ */
+static enum GNUNET_GenericReturnValue
+start_melt (struct TALER_EXCHANGE_MeltHandle *mh)
+{
+  const struct TALER_EXCHANGE_Keys *key_state;
+  json_t *melt_obj;
+  CURL *eh;
+  char arg_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2 + 32];
+  struct TALER_DenominationHashP h_denom_pub;
+  struct TALER_ExchangeWithdrawValues alg_values[mh->rd->fresh_pks_len];
+
+  for (unsigned int i = 0; i<mh->rd->fresh_pks_len; i++)
+    alg_values[i] = mh->mbds[i].alg_value;
+  if (GNUNET_OK !=
+      TALER_EXCHANGE_get_melt_data_ (&mh->rms,
+                                     mh->rd,
+                                     alg_values,
+                                     &mh->md))
+  {
+    GNUNET_break (0);
+    return GNUNET_SYSERR;
+  }
+  TALER_denom_pub_hash (&mh->md.melted_coin.pub_key,
+                        &h_denom_pub);
+  TALER_wallet_melt_sign (
+    &mh->md.melted_coin.melt_amount_with_fee,
+    &mh->md.melted_coin.fee_melt,
+    &mh->md.rc,
+    &h_denom_pub,
+    mh->md.melted_coin.h_age_commitment,
+    &mh->md.melted_coin.coin_priv,
+    &mh->coin_sig);
+  GNUNET_CRYPTO_eddsa_key_get_public (&mh->md.melted_coin.coin_priv.eddsa_priv,
+                                      &mh->coin_pub.eddsa_pub);
+  melt_obj = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_data_auto ("denom_pub_hash",
+                                &h_denom_pub),
+    TALER_JSON_pack_denom_sig ("denom_sig",
+                               &mh->md.melted_coin.sig),
+    GNUNET_JSON_pack_data_auto ("confirm_sig",
+                                &mh->coin_sig),
+    TALER_JSON_pack_amount ("value_with_fee",
+                            &mh->md.melted_coin.melt_amount_with_fee),
+    GNUNET_JSON_pack_data_auto ("rc",
+                                &mh->md.rc),
+    GNUNET_JSON_pack_allow_null (
+      (NULL != mh->md.melted_coin.h_age_commitment)
+      ? GNUNET_JSON_pack_data_auto ("age_commitment_hash",
+                                    mh->md.melted_coin.h_age_commitment)
+      : GNUNET_JSON_pack_string ("age_commitment_hash",
+                                 NULL)),
+    GNUNET_JSON_pack_allow_null (
+      mh->send_rms
+      ? GNUNET_JSON_pack_data_auto ("rms",
+                                    &mh->rms)
+      : GNUNET_JSON_pack_string ("rms",
+                                 NULL)));
+  {
+    char pub_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2];
+    char *end;
+
+    end = GNUNET_STRINGS_data_to_string (
+      &mh->coin_pub,
+      sizeof (struct TALER_CoinSpendPublicKeyP),
+      pub_str,
+      sizeof (pub_str));
+    *end = '\0';
+    GNUNET_snprintf (arg_str,
+                     sizeof (arg_str),
+                     "coins/%s/melt",
+                     pub_str);
+  }
+
+  key_state = mh->keys;
+  mh->dki = TALER_EXCHANGE_get_denomination_key (key_state,
+                                                 &mh->md.melted_coin.pub_key);
+
+  /* and now we can at last begin the actual request handling */
+
+  mh->url = TALER_url_join (mh->exchange_url,
+                            arg_str,
+                            NULL);
+  if (NULL == mh->url)
+  {
+    json_decref (melt_obj);
+    return GNUNET_SYSERR;
+  }
+  eh = TALER_EXCHANGE_curl_easy_get_ (mh->url);
+  if ( (NULL == eh) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&mh->ctx,
+                              eh,
+                              melt_obj)) )
+  {
+    GNUNET_break (0);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    json_decref (melt_obj);
+    return GNUNET_SYSERR;
+  }
+  json_decref (melt_obj);
+  mh->job = GNUNET_CURL_job_add2 (mh->cctx,
+                                  eh,
+                                  mh->ctx.headers,
+                                  &handle_melt_finished,
+                                  mh);
+  return GNUNET_OK;
+}
+
+
+/**
+ * The melt request @a mh failed, return an error to
+ * the application and cancel the operation.
+ *
+ * @param[in] mh melt request that failed
+ * @param ec error code to fail with
+ */
+static void
+fail_mh (struct TALER_EXCHANGE_MeltHandle *mh,
+         enum TALER_ErrorCode ec)
+{
+  struct TALER_EXCHANGE_MeltResponse mr = {
+    .hr.ec = ec
+  };
+
+  mh->melt_cb (mh->melt_cb_cls,
+               &mr);
+  TALER_EXCHANGE_melt_cancel (mh);
+}
+
+
+/**
+ * Callbacks of this type are used to serve the result of submitting a
+ * CS R request to a exchange.
+ *
+ * @param cls closure with our `struct TALER_EXCHANGE_MeltHandle *`
+ * @param csrr response details
+ */
+static void
+csr_cb (void *cls,
+        const struct TALER_EXCHANGE_CsRMeltResponse *csrr)
+{
+  struct TALER_EXCHANGE_MeltHandle *mh = cls;
+  unsigned int nks_off = 0;
+
+  mh->csr = NULL;
+  if (MHD_HTTP_OK != csrr->hr.http_status)
+  {
+    struct TALER_EXCHANGE_MeltResponse mr = {
+      .hr = csrr->hr
+    };
+
+    mr.hr.hint = "/csr-melt failed";
+    mh->melt_cb (mh->melt_cb_cls,
+                 &mr);
+    TALER_EXCHANGE_melt_cancel (mh);
+    return;
+  }
+  for (unsigned int i = 0; i<mh->rd->fresh_pks_len; i++)
+  {
+    const struct TALER_EXCHANGE_DenomPublicKey *fresh_pk =
+      &mh->rd->fresh_pks[i];
+    struct TALER_ExchangeWithdrawValues *wv = &mh->mbds[i].alg_value;
+
+    switch (fresh_pk->key.cipher)
+    {
+    case TALER_DENOMINATION_INVALID:
+      GNUNET_break (0);
+      fail_mh (mh,
+               TALER_EC_GENERIC_CLIENT_INTERNAL_ERROR);
+      return;
+    case TALER_DENOMINATION_RSA:
+      GNUNET_assert (TALER_DENOMINATION_RSA == wv->cipher);
+      break;
+    case TALER_DENOMINATION_CS:
+      GNUNET_assert (TALER_DENOMINATION_CS == wv->cipher);
+      *wv = csrr->details.ok.alg_values[nks_off];
+      nks_off++;
+      break;
+    }
+  }
+  mh->send_rms = true;
+  if (GNUNET_OK !=
+      start_melt (mh))
+  {
+    GNUNET_break (0);
+    fail_mh (mh,
+             TALER_EC_GENERIC_CLIENT_INTERNAL_ERROR);
+    return;
+  }
+}
+
+
+struct TALER_EXCHANGE_MeltHandle *
+TALER_EXCHANGE_melt (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  struct TALER_EXCHANGE_Keys *keys,
+  const struct TALER_RefreshMasterSecretP *rms,
+  const struct TALER_EXCHANGE_RefreshData *rd,
+  TALER_EXCHANGE_MeltCallback melt_cb,
+  void *melt_cb_cls)
+{
+  struct TALER_EXCHANGE_NonceKey nks[GNUNET_NZL (rd->fresh_pks_len)];
+  unsigned int nks_off = 0;
+  struct TALER_EXCHANGE_MeltHandle *mh;
+
+  if (0 == rd->fresh_pks_len)
+  {
+    GNUNET_break (0);
+    return NULL;
+  }
+  mh = GNUNET_new (struct TALER_EXCHANGE_MeltHandle);
+  mh->noreveal_index = TALER_CNC_KAPPA; /* invalid value */
+  mh->cctx = ctx;
+  mh->exchange_url = GNUNET_strdup (url);
+  mh->rd = rd;
+  mh->rms = *rms;
+  mh->melt_cb = melt_cb;
+  mh->melt_cb_cls = melt_cb_cls;
+  mh->mbds = GNUNET_new_array (rd->fresh_pks_len,
+                               struct TALER_EXCHANGE_MeltBlindingDetail);
+  for (unsigned int i = 0; i<rd->fresh_pks_len; i++)
+  {
+    const struct TALER_EXCHANGE_DenomPublicKey *fresh_pk = &rd->fresh_pks[i];
+    struct TALER_ExchangeWithdrawValues *wv = &mh->mbds[i].alg_value;
+
+    switch (fresh_pk->key.cipher)
+    {
+    case TALER_DENOMINATION_INVALID:
+      GNUNET_break (0);
+      GNUNET_free (mh->mbds);
+      GNUNET_free (mh);
+      return NULL;
+    case TALER_DENOMINATION_RSA:
+      wv->cipher = TALER_DENOMINATION_RSA;
+      break;
+    case TALER_DENOMINATION_CS:
+      wv->cipher = TALER_DENOMINATION_CS;
+      nks[nks_off].pk = fresh_pk;
+      nks[nks_off].cnc_num = nks_off;
+      nks_off++;
+      break;
+    }
+  }
+  mh->keys = TALER_EXCHANGE_keys_incref (keys);
+  if (0 != nks_off)
+  {
+    mh->csr = TALER_EXCHANGE_csr_melt (ctx,
+                                       url,
+                                       rms,
+                                       nks_off,
+                                       nks,
+                                       &csr_cb,
+                                       mh);
+    if (NULL == mh->csr)
+    {
+      GNUNET_break (0);
+      TALER_EXCHANGE_melt_cancel (mh);
+      return NULL;
+    }
+    return mh;
+  }
+  if (GNUNET_OK !=
+      start_melt (mh))
+  {
+    GNUNET_break (0);
+    TALER_EXCHANGE_melt_cancel (mh);
+    return NULL;
+  }
+  return mh;
+}
+
+
+void
+TALER_EXCHANGE_melt_cancel (struct TALER_EXCHANGE_MeltHandle *mh)
+{
+  if (NULL != mh->job)
+  {
+    GNUNET_CURL_job_cancel (mh->job);
+    mh->job = NULL;
+  }
+  if (NULL != mh->csr)
+  {
+    TALER_EXCHANGE_csr_melt_cancel (mh->csr);
+    mh->csr = NULL;
+  }
+  TALER_EXCHANGE_free_melt_data_ (&mh->md); /* does not free 'md' itself */
+  GNUNET_free (mh->mbds);
+  GNUNET_free (mh->url);
+  GNUNET_free (mh->exchange_url);
+  TALER_curl_easy_post_finished (&mh->ctx);
+  TALER_EXCHANGE_keys_decref (mh->keys);
+  GNUNET_free (mh);
+}
+
+
+/* end of exchange_api_melt.c */
diff --git a/src/lib/exchange_api_recoup.c b/src/lib/exchange_api_recoup.c
new file mode 100644
index 00000000..cfd265f0
--- /dev/null
+++ b/src/lib/exchange_api_recoup.c
@@ -0,0 +1,369 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2017-2023 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_recoup.c
+ * @brief Implementation of the /recoup request of the exchange's HTTP API
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <jansson.h>
+#include <microhttpd.h> /* just for HTTP status codes */
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_json_lib.h"
+#include "taler_exchange_service.h"
+#include "exchange_api_common.h"
+#include "exchange_api_handle.h"
+#include "taler_signatures.h"
+#include "exchange_api_curl_defaults.h"
+
+
+/**
+ * @brief A Recoup Handle
+ */
+struct TALER_EXCHANGE_RecoupHandle
+{
+
+  /**
+   * The keys of the exchange this request handle will use
+   */
+  struct TALER_EXCHANGE_Keys *keys;
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Context for #TEH_curl_easy_post(). Keeps the data that must
+   * persist for Curl to make the upload.
+   */
+  struct TALER_CURL_PostContext ctx;
+
+  /**
+   * Denomination key of the coin.
+   */
+  struct TALER_EXCHANGE_DenomPublicKey pk;
+
+  /**
+   * Our signature requesting the recoup.
+   */
+  struct TALER_CoinSpendSignatureP coin_sig;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_RecoupResultCallback cb;
+
+  /**
+   * Closure for @a cb.
+   */
+  void *cb_cls;
+
+  /**
+   * Public key of the coin we are trying to get paid back.
+   */
+  struct TALER_CoinSpendPublicKeyP coin_pub;
+
+};
+
+
+/**
+ * Parse a recoup response.  If it is valid, call the callback.
+ *
+ * @param ph recoup handle
+ * @param json json reply with the signature
+ * @return #GNUNET_OK if the signature is valid and we called the callback;
+ *         #GNUNET_SYSERR if not (callback must still be called)
+ */
+static enum GNUNET_GenericReturnValue
+process_recoup_response (const struct TALER_EXCHANGE_RecoupHandle *ph,
+                         const json_t *json)
+{
+  struct TALER_EXCHANGE_RecoupResponse rr = {
+    .hr.reply = json,
+    .hr.http_status = MHD_HTTP_OK
+  };
+  struct GNUNET_JSON_Specification spec_withdraw[] = {
+    GNUNET_JSON_spec_fixed_auto ("reserve_pub",
+                                 &rr.details.ok.reserve_pub),
+    GNUNET_JSON_spec_end ()
+  };
+
+  if (GNUNET_OK !=
+      GNUNET_JSON_parse (json,
+                         spec_withdraw,
+                         NULL, NULL))
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+  ph->cb (ph->cb_cls,
+          &rr);
+  return GNUNET_OK;
+}
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /recoup request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_RecoupHandle`
+ * @param response_code HTTP response code, 0 on error
+ * @param response parsed JSON result, NULL on error
+ */
+static void
+handle_recoup_finished (void *cls,
+                        long response_code,
+                        const void *response)
+{
+  struct TALER_EXCHANGE_RecoupHandle *ph = cls;
+  const json_t *j = response;
+  struct TALER_EXCHANGE_RecoupResponse rr = {
+    .hr.reply = j,
+    .hr.http_status = (unsigned int) response_code
+  };
+
+  ph->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    rr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    break;
+  case MHD_HTTP_OK:
+    if (GNUNET_OK !=
+        process_recoup_response (ph,
+                                 j))
+    {
+      GNUNET_break_op (0);
+      rr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+      rr.hr.http_status = 0;
+      break;
+    }
+    TALER_EXCHANGE_recoup_cancel (ph);
+    return;
+  case MHD_HTTP_BAD_REQUEST:
+    /* This should never happen, either us or the exchange is buggy
+       (or API version conflict); just pass JSON reply to the application */
+    rr.hr.ec = TALER_JSON_get_error_code (j);
+    rr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_CONFLICT:
+    {
+      struct TALER_Amount min_key;
+
+      rr.hr.ec = TALER_JSON_get_error_code (j);
+      rr.hr.hint = TALER_JSON_get_error_hint (j);
+      if (GNUNET_OK !=
+          TALER_EXCHANGE_get_min_denomination_ (ph->keys,
+                                                &min_key))
+      {
+        GNUNET_break (0);
+        rr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+        rr.hr.http_status = 0;
+        break;
+      }
+      break;
+    }
+  case MHD_HTTP_FORBIDDEN:
+    /* Nothing really to verify, exchange says one of the signatures is
+       invalid; as we checked them, this should never happen, we
+       should pass the JSON reply to the application */
+    rr.hr.ec = TALER_JSON_get_error_code (j);
+    rr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    /* Nothing really to verify, this should never
+       happen, we should pass the JSON reply to the application */
+    rr.hr.ec = TALER_JSON_get_error_code (j);
+    rr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_GONE:
+    /* Kind of normal: the money was already sent to the merchant
+       (it was too late for the refund). */
+    rr.hr.ec = TALER_JSON_get_error_code (j);
+    rr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_INTERNAL_SERVER_ERROR:
+    /* Server had an internal issue; we should retry, but this API
+       leaves this to the application */
+    rr.hr.ec = TALER_JSON_get_error_code (j);
+    rr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  default:
+    /* unexpected response code */
+    rr.hr.ec = TALER_JSON_get_error_code (j);
+    rr.hr.hint = TALER_JSON_get_error_hint (j);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange recoup\n",
+                (unsigned int) response_code,
+                (int) rr.hr.ec);
+    GNUNET_break (0);
+    break;
+  }
+  ph->cb (ph->cb_cls,
+          &rr);
+  TALER_EXCHANGE_recoup_cancel (ph);
+}
+
+
+struct TALER_EXCHANGE_RecoupHandle *
+TALER_EXCHANGE_recoup (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  struct TALER_EXCHANGE_Keys *keys,
+  const struct TALER_EXCHANGE_DenomPublicKey *pk,
+  const struct TALER_DenominationSignature *denom_sig,
+  const struct TALER_ExchangeWithdrawValues *exchange_vals,
+  const struct TALER_PlanchetMasterSecretP *ps,
+  TALER_EXCHANGE_RecoupResultCallback recoup_cb,
+  void *recoup_cb_cls)
+{
+  struct TALER_EXCHANGE_RecoupHandle *ph;
+  struct TALER_DenominationHashP h_denom_pub;
+  json_t *recoup_obj;
+  CURL *eh;
+  char arg_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2 + 32];
+  struct TALER_CoinSpendPrivateKeyP coin_priv;
+  union TALER_DenominationBlindingKeyP bks;
+
+  ph = GNUNET_new (struct TALER_EXCHANGE_RecoupHandle);
+  TALER_planchet_setup_coin_priv (ps,
+                                  exchange_vals,
+                                  &coin_priv);
+  TALER_planchet_blinding_secret_create (ps,
+                                         exchange_vals,
+                                         &bks);
+  GNUNET_CRYPTO_eddsa_key_get_public (&coin_priv.eddsa_priv,
+                                      &ph->coin_pub.eddsa_pub);
+  TALER_denom_pub_hash (&pk->key,
+                        &h_denom_pub);
+  TALER_wallet_recoup_sign (&h_denom_pub,
+                            &bks,
+                            &coin_priv,
+                            &ph->coin_sig);
+  recoup_obj = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_data_auto ("denom_pub_hash",
+                                &h_denom_pub),
+    TALER_JSON_pack_denom_sig ("denom_sig",
+                               denom_sig),
+    TALER_JSON_pack_exchange_withdraw_values ("ewv",
+                                              exchange_vals),
+    GNUNET_JSON_pack_data_auto ("coin_sig",
+                                &ph->coin_sig),
+    GNUNET_JSON_pack_data_auto ("coin_blind_key_secret",
+                                &bks));
+  if (TALER_DENOMINATION_CS == denom_sig->cipher)
+  {
+    struct TALER_CsNonce nonce;
+
+    /* NOTE: this is not elegant, and as per the note in TALER_coin_ev_hash()
+       it is not strictly clear that the nonce is needed. Best case would be
+       to find a way to include it more 'naturally' somehow, for example with
+       the variant union version of bks! */
+    TALER_cs_withdraw_nonce_derive (ps,
+                                    &nonce);
+    GNUNET_assert (
+      0 ==
+      json_object_set_new (recoup_obj,
+                           "cs_nonce",
+                           GNUNET_JSON_from_data_auto (
+                             &nonce)));
+  }
+
+  {
+    char pub_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2];
+    char *end;
+
+    end = GNUNET_STRINGS_data_to_string (
+      &ph->coin_pub,
+      sizeof (struct TALER_CoinSpendPublicKeyP),
+      pub_str,
+      sizeof (pub_str));
+    *end = '\0';
+    GNUNET_snprintf (arg_str,
+                     sizeof (arg_str),
+                     "coins/%s/recoup",
+                     pub_str);
+  }
+
+  ph->pk = *pk;
+  memset (&ph->pk.key,
+          0,
+          sizeof (ph->pk.key)); /* zero out, as lifetime cannot be warranted */
+  ph->cb = recoup_cb;
+  ph->cb_cls = recoup_cb_cls;
+  ph->url = TALER_url_join (url,
+                            arg_str,
+                            NULL);
+  if (NULL == ph->url)
+  {
+    json_decref (recoup_obj);
+    GNUNET_free (ph);
+    return NULL;
+  }
+  eh = TALER_EXCHANGE_curl_easy_get_ (ph->url);
+  if ( (NULL == eh) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&ph->ctx,
+                              eh,
+                              recoup_obj)) )
+  {
+    GNUNET_break (0);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    json_decref (recoup_obj);
+    GNUNET_free (ph->url);
+    GNUNET_free (ph);
+    return NULL;
+  }
+  json_decref (recoup_obj);
+  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+              "URL for recoup: `%s'\n",
+              ph->url);
+  ph->keys = TALER_EXCHANGE_keys_incref (keys);
+  ph->job = GNUNET_CURL_job_add2 (ctx,
+                                  eh,
+                                  ph->ctx.headers,
+                                  &handle_recoup_finished,
+                                  ph);
+  return ph;
+}
+
+
+void
+TALER_EXCHANGE_recoup_cancel (struct TALER_EXCHANGE_RecoupHandle *ph)
+{
+  if (NULL != ph->job)
+  {
+    GNUNET_CURL_job_cancel (ph->job);
+    ph->job = NULL;
+  }
+  GNUNET_free (ph->url);
+  TALER_curl_easy_post_finished (&ph->ctx);
+  TALER_EXCHANGE_keys_decref (ph->keys);
+  GNUNET_free (ph);
+}
+
+
+/* end of exchange_api_recoup.c */
diff --git a/src/lib/exchange_api_recoup_refresh.c 
b/src/lib/exchange_api_recoup_refresh.c
new file mode 100644
index 00000000..0bcd44de
--- /dev/null
+++ b/src/lib/exchange_api_recoup_refresh.c
@@ -0,0 +1,363 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2017-2023 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_recoup_refresh.c
+ * @brief Implementation of the /recoup-refresh request of the exchange's HTTP 
API
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <jansson.h>
+#include <microhttpd.h> /* just for HTTP status codes */
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_json_lib.h"
+#include "taler_exchange_service.h"
+#include "exchange_api_common.h"
+#include "exchange_api_handle.h"
+#include "taler_signatures.h"
+#include "exchange_api_curl_defaults.h"
+
+
+/**
+ * @brief A Recoup Handle
+ */
+struct TALER_EXCHANGE_RecoupRefreshHandle
+{
+
+  /**
+   * The keys of the exchange this request handle will use
+   */
+  struct TALER_EXCHANGE_Keys *keys;
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Context for #TEH_curl_easy_post(). Keeps the data that must
+   * persist for Curl to make the upload.
+   */
+  struct TALER_CURL_PostContext ctx;
+
+  /**
+   * Denomination key of the coin.
+   */
+  struct TALER_EXCHANGE_DenomPublicKey pk;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_RecoupRefreshResultCallback cb;
+
+  /**
+   * Closure for @a cb.
+   */
+  void *cb_cls;
+
+  /**
+   * Public key of the coin we are trying to get paid back.
+   */
+  struct TALER_CoinSpendPublicKeyP coin_pub;
+
+  /**
+   * Signature affirming the recoup-refresh operation.
+   */
+  struct TALER_CoinSpendSignatureP coin_sig;
+
+};
+
+
+/**
+ * Parse a recoup-refresh response.  If it is valid, call the callback.
+ *
+ * @param ph recoup handle
+ * @param json json reply with the signature
+ * @return #GNUNET_OK if the signature is valid and we called the callback;
+ *         #GNUNET_SYSERR if not (callback must still be called)
+ */
+static enum GNUNET_GenericReturnValue
+process_recoup_response (
+  const struct TALER_EXCHANGE_RecoupRefreshHandle *ph,
+  const json_t *json)
+{
+  struct TALER_EXCHANGE_RecoupRefreshResponse rrr = {
+    .hr.reply = json,
+    .hr.http_status = MHD_HTTP_OK
+  };
+  struct GNUNET_JSON_Specification spec_refresh[] = {
+    GNUNET_JSON_spec_fixed_auto ("old_coin_pub",
+                                 &rrr.details.ok.old_coin_pub),
+    GNUNET_JSON_spec_end ()
+  };
+
+  if (GNUNET_OK !=
+      GNUNET_JSON_parse (json,
+                         spec_refresh,
+                         NULL, NULL))
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+  ph->cb (ph->cb_cls,
+          &rrr);
+  return GNUNET_OK;
+}
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /recoup-refresh request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_RecoupRefreshHandle`
+ * @param response_code HTTP response code, 0 on error
+ * @param response parsed JSON result, NULL on error
+ */
+static void
+handle_recoup_refresh_finished (void *cls,
+                                long response_code,
+                                const void *response)
+{
+  struct TALER_EXCHANGE_RecoupRefreshHandle *ph = cls;
+  const json_t *j = response;
+  struct TALER_EXCHANGE_RecoupRefreshResponse rrr = {
+    .hr.reply = j,
+    .hr.http_status = (unsigned int) response_code
+  };
+
+  ph->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    rrr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    break;
+  case MHD_HTTP_OK:
+    if (GNUNET_OK !=
+        process_recoup_response (ph,
+                                 j))
+    {
+      GNUNET_break_op (0);
+      rrr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+      rrr.hr.http_status = 0;
+      break;
+    }
+    TALER_EXCHANGE_recoup_refresh_cancel (ph);
+    return;
+  case MHD_HTTP_BAD_REQUEST:
+    /* This should never happen, either us or the exchange is buggy
+       (or API version conflict); just pass JSON reply to the application */
+    rrr.hr.ec = TALER_JSON_get_error_code (j);
+    rrr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    /* Nothing really to verify, exchange says one of the signatures is
+       invalid; as we checked them, this should never happen, we
+       should pass the JSON reply to the application */
+    rrr.hr.ec = TALER_JSON_get_error_code (j);
+    rrr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    /* Nothing really to verify, this should never
+       happen, we should pass the JSON reply to the application */
+    rrr.hr.ec = TALER_JSON_get_error_code (j);
+    rrr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_CONFLICT:
+    rrr.hr.ec = TALER_JSON_get_error_code (j);
+    rrr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_GONE:
+    /* Kind of normal: the money was already sent to the merchant
+       (it was too late for the refund). */
+    rrr.hr.ec = TALER_JSON_get_error_code (j);
+    rrr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_INTERNAL_SERVER_ERROR:
+    /* Server had an internal issue; we should retry, but this API
+       leaves this to the application */
+    rrr.hr.ec = TALER_JSON_get_error_code (j);
+    rrr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  default:
+    /* unexpected response code */
+    rrr.hr.ec = TALER_JSON_get_error_code (j);
+    rrr.hr.hint = TALER_JSON_get_error_hint (j);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange recoup\n",
+                (unsigned int) response_code,
+                (int) rrr.hr.ec);
+    GNUNET_break (0);
+    break;
+  }
+  ph->cb (ph->cb_cls,
+          &rrr);
+  TALER_EXCHANGE_recoup_refresh_cancel (ph);
+}
+
+
+struct TALER_EXCHANGE_RecoupRefreshHandle *
+TALER_EXCHANGE_recoup_refresh (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  struct TALER_EXCHANGE_Keys *keys,
+  const struct TALER_EXCHANGE_DenomPublicKey *pk,
+  const struct TALER_DenominationSignature *denom_sig,
+  const struct TALER_ExchangeWithdrawValues *exchange_vals,
+  const struct TALER_RefreshMasterSecretP *rms,
+  const struct TALER_PlanchetMasterSecretP *ps,
+  unsigned int idx,
+  TALER_EXCHANGE_RecoupRefreshResultCallback recoup_cb,
+  void *recoup_cb_cls)
+{
+  struct TALER_EXCHANGE_RecoupRefreshHandle *ph;
+  struct TALER_DenominationHashP h_denom_pub;
+  json_t *recoup_obj;
+  CURL *eh;
+  char arg_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2 + 32];
+  struct TALER_CoinSpendPrivateKeyP coin_priv;
+  union TALER_DenominationBlindingKeyP bks;
+
+  GNUNET_assert (NULL != recoup_cb);
+  ph = GNUNET_new (struct TALER_EXCHANGE_RecoupRefreshHandle);
+  ph->pk = *pk;
+  memset (&ph->pk.key,
+          0,
+          sizeof (ph->pk.key)); /* zero out, as lifetime cannot be warranted */
+  ph->cb = recoup_cb;
+  ph->cb_cls = recoup_cb_cls;
+  TALER_planchet_setup_coin_priv (ps,
+                                  exchange_vals,
+                                  &coin_priv);
+  TALER_planchet_blinding_secret_create (ps,
+                                         exchange_vals,
+                                         &bks);
+  GNUNET_CRYPTO_eddsa_key_get_public (&coin_priv.eddsa_priv,
+                                      &ph->coin_pub.eddsa_pub);
+  TALER_denom_pub_hash (&pk->key,
+                        &h_denom_pub);
+  TALER_wallet_recoup_refresh_sign (&h_denom_pub,
+                                    &bks,
+                                    &coin_priv,
+                                    &ph->coin_sig);
+  recoup_obj = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_data_auto ("denom_pub_hash",
+                                &h_denom_pub),
+    TALER_JSON_pack_denom_sig ("denom_sig",
+                               denom_sig),
+    TALER_JSON_pack_exchange_withdraw_values ("ewv",
+                                              exchange_vals),
+    GNUNET_JSON_pack_data_auto ("coin_sig",
+                                &ph->coin_sig),
+    GNUNET_JSON_pack_data_auto ("coin_blind_key_secret",
+                                &bks));
+
+  if (TALER_DENOMINATION_CS == denom_sig->cipher)
+  {
+    struct TALER_CsNonce nonce;
+
+    /* NOTE: this is not elegant, and as per the note in TALER_coin_ev_hash()
+       it is not strictly clear that the nonce is needed. Best case would be
+       to find a way to include it more 'naturally' somehow, for example with
+       the variant union version of bks! */
+    TALER_cs_refresh_nonce_derive (rms,
+                                   idx,
+                                   &nonce);
+    GNUNET_assert (
+      0 ==
+      json_object_set_new (recoup_obj,
+                           "cs_nonce",
+                           GNUNET_JSON_from_data_auto (
+                             &nonce)));
+  }
+
+  {
+    char pub_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2];
+    char *end;
+
+    end = GNUNET_STRINGS_data_to_string (
+      &ph->coin_pub,
+      sizeof (struct TALER_CoinSpendPublicKeyP),
+      pub_str,
+      sizeof (pub_str));
+    *end = '\0';
+    GNUNET_snprintf (arg_str,
+                     sizeof (arg_str),
+                     "coins/%s/recoup-refresh",
+                     pub_str);
+  }
+
+  ph->url = TALER_url_join (url,
+                            arg_str,
+                            NULL);
+  if (NULL == ph->url)
+  {
+    json_decref (recoup_obj);
+    GNUNET_free (ph);
+    return NULL;
+  }
+  eh = TALER_EXCHANGE_curl_easy_get_ (ph->url);
+  if ( (NULL == eh) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&ph->ctx,
+                              eh,
+                              recoup_obj)) )
+  {
+    GNUNET_break (0);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    json_decref (recoup_obj);
+    GNUNET_free (ph->url);
+    GNUNET_free (ph);
+    return NULL;
+  }
+  json_decref (recoup_obj);
+  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+              "URL for recoup-refresh: `%s'\n",
+              ph->url);
+  ph->keys = TALER_EXCHANGE_keys_incref (keys);
+  ph->job = GNUNET_CURL_job_add2 (ctx,
+                                  eh,
+                                  ph->ctx.headers,
+                                  &handle_recoup_refresh_finished,
+                                  ph);
+  return ph;
+}
+
+
+void
+TALER_EXCHANGE_recoup_refresh_cancel (
+  struct TALER_EXCHANGE_RecoupRefreshHandle *ph)
+{
+  if (NULL != ph->job)
+  {
+    GNUNET_CURL_job_cancel (ph->job);
+    ph->job = NULL;
+  }
+  GNUNET_free (ph->url);
+  TALER_curl_easy_post_finished (&ph->ctx);
+  TALER_EXCHANGE_keys_decref (ph->keys);
+  GNUNET_free (ph);
+}
+
+
+/* end of exchange_api_recoup_refresh.c */
diff --git a/src/lib/exchange_api_refresh_common.c 
b/src/lib/exchange_api_refresh_common.c
new file mode 100644
index 00000000..0a6665b5
--- /dev/null
+++ b/src/lib/exchange_api_refresh_common.c
@@ -0,0 +1,249 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2015-2022 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_refresh_common.c
+ * @brief Serialization logic shared between melt and reveal steps during 
refreshing
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "exchange_api_refresh_common.h"
+
+
+void
+TALER_EXCHANGE_free_melt_data_ (struct MeltData *md)
+{
+  for (unsigned int i = 0; i < TALER_CNC_KAPPA; i++)
+  {
+    struct TALER_RefreshCoinData *rcds = md->rcd[i];
+
+    if (NULL == rcds)
+      continue;
+    for (unsigned int j = 0; j < md->num_fresh_coins; j++)
+      TALER_blinded_planchet_free (&rcds[j].blinded_planchet);
+    GNUNET_free (rcds);
+  }
+  TALER_denom_pub_free (&md->melted_coin.pub_key);
+  TALER_denom_sig_free (&md->melted_coin.sig);
+  if (NULL != md->fcds)
+  {
+    for (unsigned int j = 0; j<md->num_fresh_coins; j++)
+    {
+      struct FreshCoinData *fcd = &md->fcds[j];
+
+      TALER_denom_pub_free (&fcd->fresh_pk);
+      for (size_t i = 0; i < TALER_CNC_KAPPA; i++)
+      {
+        TALER_age_commitment_proof_free (fcd->age_commitment_proofs[i]);
+        GNUNET_free (fcd->age_commitment_proofs[i]);
+      }
+    }
+    GNUNET_free (md->fcds);
+  }
+  /* Finally, clean up a bit... */
+  GNUNET_CRYPTO_zero_keys (md,
+                           sizeof (struct MeltData));
+}
+
+
+enum GNUNET_GenericReturnValue
+TALER_EXCHANGE_get_melt_data_ (
+  const struct TALER_RefreshMasterSecretP *rms,
+  const struct TALER_EXCHANGE_RefreshData *rd,
+  const struct TALER_ExchangeWithdrawValues *alg_values,
+  struct MeltData *md)
+{
+  struct TALER_Amount total;
+  struct TALER_CoinSpendPublicKeyP coin_pub;
+  struct TALER_CsNonce nonces[rd->fresh_pks_len];
+  bool uses_cs = false;
+
+  GNUNET_CRYPTO_eddsa_key_get_public (&rd->melt_priv.eddsa_priv,
+                                      &coin_pub.eddsa_pub);
+  /* build up melt data structure */
+  memset (md,
+          0,
+          sizeof (*md));
+  md->num_fresh_coins = rd->fresh_pks_len;
+  md->melted_coin.coin_priv = rd->melt_priv;
+  md->melted_coin.melt_amount_with_fee = rd->melt_amount;
+  md->melted_coin.fee_melt = rd->melt_pk.fees.refresh;
+  md->melted_coin.original_value = rd->melt_pk.value;
+  md->melted_coin.expire_deposit = rd->melt_pk.expire_deposit;
+  md->melted_coin.age_commitment_proof = rd->melt_age_commitment_proof;
+  md->melted_coin.h_age_commitment = rd->melt_h_age_commitment;
+
+  GNUNET_assert (GNUNET_OK ==
+                 TALER_amount_set_zero (rd->melt_amount.currency,
+                                        &total));
+  TALER_denom_pub_deep_copy (&md->melted_coin.pub_key,
+                             &rd->melt_pk.key);
+  TALER_denom_sig_deep_copy (&md->melted_coin.sig,
+                             &rd->melt_sig);
+  md->fcds = GNUNET_new_array (md->num_fresh_coins,
+                               struct FreshCoinData);
+  for (unsigned int j = 0; j<rd->fresh_pks_len; j++)
+  {
+    struct FreshCoinData *fcd = &md->fcds[j];
+
+    if (alg_values[j].cipher != rd->fresh_pks[j].key.cipher)
+    {
+      GNUNET_break (0);
+      TALER_EXCHANGE_free_melt_data_ (md);
+      return GNUNET_SYSERR;
+    }
+    if (TALER_DENOMINATION_CS == alg_values[j].cipher)
+    {
+      uses_cs = true;
+      TALER_cs_refresh_nonce_derive (
+        rms,
+        j,
+        &nonces[j]);
+    }
+    TALER_denom_pub_deep_copy (&fcd->fresh_pk,
+                               &rd->fresh_pks[j].key);
+    if ( (0 >
+          TALER_amount_add (&total,
+                            &total,
+                            &rd->fresh_pks[j].value)) ||
+         (0 >
+          TALER_amount_add (&total,
+                            &total,
+                            &rd->fresh_pks[j].fees.withdraw)) )
+    {
+      GNUNET_break (0);
+      TALER_EXCHANGE_free_melt_data_ (md);
+      return GNUNET_SYSERR;
+    }
+  }
+
+  /* verify that melt_amount is above total cost */
+  if (1 ==
+      TALER_amount_cmp (&total,
+                        &rd->melt_amount) )
+  {
+    /* Eh, this operation is more expensive than the
+       @a melt_amount. This is not OK. */
+    GNUNET_break (0);
+    TALER_EXCHANGE_free_melt_data_ (md);
+    return GNUNET_SYSERR;
+  }
+
+  /* build up coins */
+  for (unsigned int i = 0; i<TALER_CNC_KAPPA; i++)
+  {
+    struct TALER_TransferSecretP trans_sec;
+
+    TALER_planchet_secret_to_transfer_priv (
+      rms,
+      &rd->melt_priv,
+      i,
+      &md->transfer_priv[i]);
+
+    GNUNET_CRYPTO_ecdhe_key_get_public (
+      &md->transfer_priv[i].ecdhe_priv,
+      &md->transfer_pub[i].ecdhe_pub);
+
+    TALER_link_derive_transfer_secret (&rd->melt_priv,
+                                       &md->transfer_priv[i],
+                                       &trans_sec);
+
+    md->rcd[i] = GNUNET_new_array (rd->fresh_pks_len,
+                                   struct TALER_RefreshCoinData);
+
+    for (unsigned int j = 0; j<rd->fresh_pks_len; j++)
+    {
+      struct FreshCoinData *fcd = &md->fcds[j];
+      struct TALER_CoinSpendPrivateKeyP *coin_priv = &fcd->coin_priv;
+      struct TALER_PlanchetMasterSecretP *ps = &fcd->ps[i];
+      struct TALER_RefreshCoinData *rcd = &md->rcd[i][j];
+      union TALER_DenominationBlindingKeyP *bks = &fcd->bks[i];
+      struct TALER_PlanchetDetail pd;
+      struct TALER_CoinPubHashP c_hash;
+      struct TALER_AgeCommitmentHash ach;
+      struct TALER_AgeCommitmentHash *pah = NULL;
+
+      TALER_transfer_secret_to_planchet_secret (&trans_sec,
+                                                j,
+                                                ps);
+
+      TALER_planchet_setup_coin_priv (ps,
+                                      &alg_values[j],
+                                      coin_priv);
+
+      TALER_planchet_blinding_secret_create (ps,
+                                             &alg_values[j],
+                                             bks);
+
+      if (NULL != rd->melt_age_commitment_proof)
+      {
+        fcd->age_commitment_proofs[i] = GNUNET_new (struct
+                                                    TALER_AgeCommitmentProof);
+
+        GNUNET_assert (GNUNET_OK ==
+                       TALER_age_commitment_derive (
+                         md->melted_coin.age_commitment_proof,
+                         &trans_sec.key,
+                         fcd->age_commitment_proofs[i]));
+
+        TALER_age_commitment_hash (
+          &fcd->age_commitment_proofs[i]->commitment,
+          &ach);
+        pah = &ach;
+      }
+
+      if (TALER_DENOMINATION_CS == alg_values[j].cipher)
+        pd.blinded_planchet.details.cs_blinded_planchet.nonce = nonces[j];
+
+      if (GNUNET_OK !=
+          TALER_planchet_prepare (&fcd->fresh_pk,
+                                  &alg_values[j],
+                                  bks,
+                                  coin_priv,
+                                  pah,
+                                  &c_hash,
+                                  &pd))
+      {
+        GNUNET_break_op (0);
+        TALER_EXCHANGE_free_melt_data_ (md);
+        return GNUNET_SYSERR;
+      }
+      rcd->blinded_planchet = pd.blinded_planchet;
+      rcd->dk = &fcd->fresh_pk;
+    }
+  }
+
+  /* Finally, compute refresh commitment */
+  {
+    struct TALER_RefreshCommitmentEntry rce[TALER_CNC_KAPPA];
+
+    for (unsigned int i = 0; i<TALER_CNC_KAPPA; i++)
+    {
+      rce[i].transfer_pub = md->transfer_pub[i];
+      rce[i].new_coins = md->rcd[i];
+    }
+    TALER_refresh_get_commitment (&md->rc,
+                                  TALER_CNC_KAPPA,
+                                  uses_cs
+                                  ? rms
+                                  : NULL,
+                                  rd->fresh_pks_len,
+                                  rce,
+                                  &coin_pub,
+                                  &rd->melt_amount);
+  }
+  return GNUNET_OK;
+}
diff --git a/src/lib/exchange_api_refresh_common.h 
b/src/lib/exchange_api_refresh_common.h
new file mode 100644
index 00000000..0cb80f17
--- /dev/null
+++ b/src/lib/exchange_api_refresh_common.h
@@ -0,0 +1,201 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2015-2022 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_refresh_common.h
+ * @brief shared (serialization) logic for refresh protocol
+ * @author Christian Grothoff
+ */
+#ifndef REFRESH_COMMON_H
+#define REFRESH_COMMON_H
+#include <jansson.h>
+#include "taler_json_lib.h"
+#include "taler_exchange_service.h"
+#include "taler_signatures.h"
+
+
+/**
+ * Information about a coin we are melting.
+ */
+struct MeltedCoin
+{
+  /**
+   * Private key of the coin.
+   */
+  struct TALER_CoinSpendPrivateKeyP coin_priv;
+
+  /**
+   * Amount this coin contributes to the melt, including fee.
+   */
+  struct TALER_Amount melt_amount_with_fee;
+
+  /**
+   * The applicable fee for melting a coin of this denomination
+   */
+  struct TALER_Amount fee_melt;
+
+  /**
+   * The original value of the coin.
+   */
+  struct TALER_Amount original_value;
+
+  /**
+   * The original age commitment, its proof and its hash.  MUST be NULL if no
+   * age commitment was set.
+   */
+  const struct TALER_AgeCommitmentProof *age_commitment_proof;
+  const struct TALER_AgeCommitmentHash *h_age_commitment;
+
+  /**
+   * Timestamp indicating when coins of this denomination become invalid.
+   */
+  struct GNUNET_TIME_Timestamp expire_deposit;
+
+  /**
+   * Denomination key of the original coin.
+   */
+  struct TALER_DenominationPublicKey pub_key;
+
+  /**
+   * Exchange's signature over the coin.
+   */
+  struct TALER_DenominationSignature sig;
+
+};
+
+
+/**
+ * Data we keep for each fresh coin created in the
+ * melt process.
+ */
+struct FreshCoinData
+{
+  /**
+   * Denomination public key of the coin.
+   */
+  struct TALER_DenominationPublicKey fresh_pk;
+
+  /**
+   * Array of planchet secrets for the coins, depending
+   * on the cut-and-choose.
+   */
+  struct TALER_PlanchetMasterSecretP ps[TALER_CNC_KAPPA];
+
+  /**
+   * Private key of the coin.
+   */
+  struct TALER_CoinSpendPrivateKeyP coin_priv;
+
+  /**
+   * Arrays of age commitments and proofs to be created, one for each
+   * cut-and-choose dimension.  NULL if age restriction is not applicable.
+   */
+  struct TALER_AgeCommitmentProof *age_commitment_proofs[TALER_CNC_KAPPA];
+
+  /**
+   * Blinding key secrets for the coins, depending on the
+   * cut-and-choose.
+   */
+  union TALER_DenominationBlindingKeyP bks[TALER_CNC_KAPPA];
+
+};
+
+
+/**
+ * Melt data in non-serialized format for convenient processing.
+ */
+struct MeltData
+{
+
+  /**
+   * Hash over the committed data during refresh operation.
+   */
+  struct TALER_RefreshCommitmentP rc;
+
+  /**
+   * Information about the melted coin.
+   */
+  struct MeltedCoin melted_coin;
+
+  /**
+   * Array of length @e num_fresh_coins with information
+   * about each fresh coin.
+   */
+  struct FreshCoinData *fcds;
+
+  /**
+   * Transfer secrets, one per cut and choose.
+   */
+  struct TALER_TransferSecretP trans_sec[TALER_CNC_KAPPA];
+
+  /**
+   * Transfer private keys for each cut-and-choose dimension.
+   */
+  struct TALER_TransferPrivateKeyP transfer_priv[TALER_CNC_KAPPA];
+
+  /**
+   * Transfer public key of this commitment.
+   */
+  struct TALER_TransferPublicKeyP transfer_pub[TALER_CNC_KAPPA];
+
+  /**
+   * Transfer secrets, one per cut and choose.
+   */
+  struct TALER_RefreshCommitmentEntry rce[TALER_CNC_KAPPA];
+
+  /**
+   * Blinded planchets and denominations of the fresh coins, depending on the 
cut-and-choose.  Array of length
+   * @e num_fresh_coins.
+   */
+  struct TALER_RefreshCoinData *rcd[TALER_CNC_KAPPA];
+
+  /**
+   * Number of coins we are creating
+   */
+  uint16_t num_fresh_coins;
+
+};
+
+
+/**
+ * Compute the melt data from the refresh data and secret.
+ *
+ * @param rms secret internals of the refresh-reveal operation
+ * @param rd refresh data with the characteristics of the operation
+ * @param alg_values contributions from the exchange into the melt
+ * @param[out] md where to write the derived melt data
+ */
+enum GNUNET_GenericReturnValue
+TALER_EXCHANGE_get_melt_data_ (
+  const struct TALER_RefreshMasterSecretP *rms,
+  const struct TALER_EXCHANGE_RefreshData *rd,
+  const struct TALER_ExchangeWithdrawValues *alg_values,
+  struct MeltData *md);
+
+
+/**
+ * Free all information associated with a melting session.  Note
+ * that we allow the melting session to be only partially initialized,
+ * as we use this function also when freeing melt data that was not
+ * fully initialized.
+ *
+ * @param[in] md melting data to release, the pointer itself is NOT
+ *           freed (as it is typically not allocated by itself)
+ */
+void
+TALER_EXCHANGE_free_melt_data_ (struct MeltData *md);
+
+#endif
diff --git a/src/lib/exchange_api_refreshes_reveal.c 
b/src/lib/exchange_api_refreshes_reveal.c
new file mode 100644
index 00000000..22068299
--- /dev/null
+++ b/src/lib/exchange_api_refreshes_reveal.c
@@ -0,0 +1,531 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2015-2023 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_refreshes_reveal.c
+ * @brief Implementation of the /refreshes/$RCH/reveal requests
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <jansson.h>
+#include <microhttpd.h> /* just for HTTP status codes */
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_json_lib.h"
+#include "taler_exchange_service.h"
+#include "exchange_api_handle.h"
+#include "taler_signatures.h"
+#include "exchange_api_curl_defaults.h"
+#include "exchange_api_refresh_common.h"
+
+
+/**
+ * @brief A /refreshes/$RCH/reveal Handle
+ */
+struct TALER_EXCHANGE_RefreshesRevealHandle
+{
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Context for #TEH_curl_easy_post(). Keeps the data that must
+   * persist for Curl to make the upload.
+   */
+  struct TALER_CURL_PostContext ctx;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Exchange-contributed values to the operation.
+   */
+  struct TALER_ExchangeWithdrawValues *alg_values;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_RefreshesRevealCallback reveal_cb;
+
+  /**
+   * Closure for @e reveal_cb.
+   */
+  void *reveal_cb_cls;
+
+  /**
+   * Actual information about the melt operation.
+   */
+  struct MeltData md;
+
+  /**
+   * The index selected by the exchange in cut-and-choose to not be revealed.
+   */
+  uint16_t noreveal_index;
+
+};
+
+
+/**
+ * We got a 200 OK response for the /refreshes/$RCH/reveal operation.  Extract
+ * the coin signatures and return them to the caller.  The signatures we get
+ * from the exchange is for the blinded value.  Thus, we first must unblind
+ * them and then should verify their validity.
+ *
+ * If everything checks out, we return the unblinded signatures
+ * to the application via the callback.
+ *
+ * @param rrh operation handle
+ * @param json reply from the exchange
+ * @param[out] rcis array of length `num_fresh_coins`, initialized to contain 
the coin data
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors
+ */
+static enum GNUNET_GenericReturnValue
+refresh_reveal_ok (struct TALER_EXCHANGE_RefreshesRevealHandle *rrh,
+                   const json_t *json,
+                   struct TALER_EXCHANGE_RevealedCoinInfo *rcis)
+{
+  const json_t *jsona;
+  struct GNUNET_JSON_Specification outer_spec[] = {
+    GNUNET_JSON_spec_array_const ("ev_sigs",
+                                  &jsona),
+    GNUNET_JSON_spec_end ()
+  };
+
+  if (GNUNET_OK !=
+      GNUNET_JSON_parse (json,
+                         outer_spec,
+                         NULL, NULL))
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+  if (rrh->md.num_fresh_coins != json_array_size (jsona))
+  {
+    /* Number of coins generated does not match our expectation */
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+  for (unsigned int i = 0; i<rrh->md.num_fresh_coins; i++)
+  {
+    struct TALER_EXCHANGE_RevealedCoinInfo *rci = &rcis[i];
+    const struct FreshCoinData *fcd = &rrh->md.fcds[i];
+    const struct TALER_DenominationPublicKey *pk;
+    json_t *jsonai;
+    struct TALER_BlindedDenominationSignature blind_sig;
+    struct TALER_CoinSpendPublicKeyP coin_pub;
+    struct TALER_CoinPubHashP coin_hash;
+    struct GNUNET_JSON_Specification spec[] = {
+      TALER_JSON_spec_blinded_denom_sig ("ev_sig",
+                                         &blind_sig),
+      GNUNET_JSON_spec_end ()
+    };
+    struct TALER_FreshCoin coin;
+    union TALER_DenominationBlindingKeyP bks;
+    const struct TALER_AgeCommitmentHash *pah = NULL;
+
+    rci->ps = fcd->ps[rrh->noreveal_index];
+    rci->bks = fcd->bks[rrh->noreveal_index];
+    rci->age_commitment_proof = NULL;
+
+    pk = &fcd->fresh_pk;
+    jsonai = json_array_get (jsona, i);
+
+    GNUNET_assert (NULL != jsonai);
+
+    if (NULL != rrh->md.melted_coin.age_commitment_proof)
+    {
+      rci->age_commitment_proof =
+        fcd->age_commitment_proofs[rrh->noreveal_index];
+
+      TALER_age_commitment_hash (&rci->age_commitment_proof->commitment,
+                                 &rci->h_age_commitment);
+      pah = &rci->h_age_commitment;
+    }
+
+    if (GNUNET_OK !=
+        GNUNET_JSON_parse (jsonai,
+                           spec,
+                           NULL, NULL))
+    {
+      GNUNET_break_op (0);
+      return GNUNET_SYSERR;
+    }
+
+    TALER_planchet_setup_coin_priv (&rci->ps,
+                                    &rrh->alg_values[i],
+                                    &rci->coin_priv);
+    TALER_planchet_blinding_secret_create (&rci->ps,
+                                           &rrh->alg_values[i],
+                                           &bks);
+    /* needed to verify the signature, and we didn't store it earlier,
+       hence recomputing it here... */
+    GNUNET_CRYPTO_eddsa_key_get_public (&rci->coin_priv.eddsa_priv,
+                                        &coin_pub.eddsa_pub);
+    TALER_coin_pub_hash (
+      &coin_pub,
+      pah,
+      &coin_hash);
+    if (GNUNET_OK !=
+        TALER_planchet_to_coin (
+          pk,
+          &blind_sig,
+          &bks,
+          &rci->coin_priv,
+          pah,
+          &coin_hash,
+          &rrh->alg_values[i],
+          &coin))
+    {
+      GNUNET_break_op (0);
+      GNUNET_JSON_parse_free (spec);
+      return GNUNET_SYSERR;
+    }
+    GNUNET_JSON_parse_free (spec);
+    rci->sig = coin.sig;
+  }
+  return GNUNET_OK;
+}
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /refreshes/$RCH/reveal request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_RefreshHandle`
+ * @param response_code HTTP response code, 0 on error
+ * @param response parsed JSON result, NULL on error
+ */
+static void
+handle_refresh_reveal_finished (void *cls,
+                                long response_code,
+                                const void *response)
+{
+  struct TALER_EXCHANGE_RefreshesRevealHandle *rrh = cls;
+  const json_t *j = response;
+  struct TALER_EXCHANGE_RevealResult rr = {
+    .hr.reply = j,
+    .hr.http_status = (unsigned int) response_code
+  };
+
+  rrh->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    rr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    break;
+  case MHD_HTTP_OK:
+    {
+      struct TALER_EXCHANGE_RevealedCoinInfo rcis[rrh->md.num_fresh_coins];
+      enum GNUNET_GenericReturnValue ret;
+
+      memset (rcis,
+              0,
+              sizeof (rcis));
+      ret = refresh_reveal_ok (rrh,
+                               j,
+                               rcis);
+      if (GNUNET_OK != ret)
+      {
+        rr.hr.http_status = 0;
+        rr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+        break;
+      }
+      else
+      {
+        GNUNET_assert (rrh->noreveal_index < TALER_CNC_KAPPA);
+        rr.details.ok.num_coins = rrh->md.num_fresh_coins;
+        rr.details.ok.coins = rcis;
+        rrh->reveal_cb (rrh->reveal_cb_cls,
+                        &rr);
+        rrh->reveal_cb = NULL;
+      }
+      for (unsigned int i = 0; i<rrh->md.num_fresh_coins; i++)
+      {
+        TALER_denom_sig_free (&rcis[i].sig);
+        TALER_age_commitment_proof_free (rcis[i].age_commitment_proof);
+      }
+      TALER_EXCHANGE_refreshes_reveal_cancel (rrh);
+      return;
+    }
+  case MHD_HTTP_BAD_REQUEST:
+    /* This should never happen, either us or the exchange is buggy
+       (or API version conflict); just pass JSON reply to the application */
+    rr.hr.ec = TALER_JSON_get_error_code (j);
+    rr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_CONFLICT:
+    /* Nothing really to verify, exchange says our reveal is inconsistent
+       with our commitment, so either side is buggy; we
+       should pass the JSON reply to the application */
+    rr.hr.ec = TALER_JSON_get_error_code (j);
+    rr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_GONE:
+    /* Server claims key expired or has been revoked */
+    rr.hr.ec = TALER_JSON_get_error_code (j);
+    rr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_INTERNAL_SERVER_ERROR:
+    /* Server had an internal issue; we should retry, but this API
+       leaves this to the application */
+    rr.hr.ec = TALER_JSON_get_error_code (j);
+    rr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    rr.hr.ec = TALER_JSON_get_error_code (j);
+    rr.hr.hint = TALER_JSON_get_error_hint (j);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange refreshes 
reveal\n",
+                (unsigned int) response_code,
+                (int) rr.hr.ec);
+    break;
+  }
+  if (NULL != rrh->reveal_cb)
+    rrh->reveal_cb (rrh->reveal_cb_cls,
+                    &rr);
+  TALER_EXCHANGE_refreshes_reveal_cancel (rrh);
+}
+
+
+struct TALER_EXCHANGE_RefreshesRevealHandle *
+TALER_EXCHANGE_refreshes_reveal (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  const struct TALER_RefreshMasterSecretP *rms,
+  const struct TALER_EXCHANGE_RefreshData *rd,
+  unsigned int num_coins,
+  const struct TALER_ExchangeWithdrawValues alg_values[static num_coins],
+  uint32_t noreveal_index,
+  TALER_EXCHANGE_RefreshesRevealCallback reveal_cb,
+  void *reveal_cb_cls)
+{
+  struct TALER_EXCHANGE_RefreshesRevealHandle *rrh;
+  json_t *transfer_privs;
+  json_t *new_denoms_h;
+  json_t *coin_evs;
+  json_t *reveal_obj;
+  json_t *link_sigs;
+  json_t *old_age_commitment = NULL;
+  CURL *eh;
+  struct MeltData md;
+  char arg_str[sizeof (struct TALER_RefreshCommitmentP) * 2 + 32];
+  bool send_rms = false;
+
+  GNUNET_assert (num_coins == rd->fresh_pks_len);
+  if (noreveal_index >= TALER_CNC_KAPPA)
+  {
+    /* We check this here, as it would be really bad to below just
+       disclose all the transfer keys. Note that this error should
+       have been caught way earlier when the exchange replied, but maybe
+       we had some internal corruption that changed the value... */
+    GNUNET_break (0);
+    return NULL;
+  }
+  if (GNUNET_OK !=
+      TALER_EXCHANGE_get_melt_data_ (rms,
+                                     rd,
+                                     alg_values,
+                                     &md))
+  {
+    GNUNET_break (0);
+    return NULL;
+  }
+
+  /* now new_denoms */
+  GNUNET_assert (NULL != (new_denoms_h = json_array ()));
+  GNUNET_assert (NULL != (coin_evs = json_array ()));
+  GNUNET_assert (NULL != (link_sigs = json_array ()));
+  for (unsigned int i = 0; i<md.num_fresh_coins; i++)
+  {
+    const struct TALER_RefreshCoinData *rcd = &md.rcd[noreveal_index][i];
+    struct TALER_DenominationHashP denom_hash;
+
+    if (TALER_DENOMINATION_CS == md.fcds[i].fresh_pk.cipher)
+      send_rms = true;
+    TALER_denom_pub_hash (&md.fcds[i].fresh_pk,
+                          &denom_hash);
+    GNUNET_assert (0 ==
+                   json_array_append_new (new_denoms_h,
+                                          GNUNET_JSON_from_data_auto (
+                                            &denom_hash)));
+    GNUNET_assert (0 ==
+                   json_array_append_new (
+                     coin_evs,
+                     GNUNET_JSON_PACK (
+                       TALER_JSON_pack_blinded_planchet (
+                         NULL,
+                         &rcd->blinded_planchet))));
+    {
+      struct TALER_CoinSpendSignatureP link_sig;
+      struct TALER_BlindedCoinHashP bch;
+
+      TALER_coin_ev_hash (&rcd->blinded_planchet,
+                          &denom_hash,
+                          &bch);
+      TALER_wallet_link_sign (
+        &denom_hash,
+        &md.transfer_pub[noreveal_index],
+        &bch,
+        &md.melted_coin.coin_priv,
+        &link_sig);
+      GNUNET_assert (0 ==
+                     json_array_append_new (
+                       link_sigs,
+                       GNUNET_JSON_from_data_auto (&link_sig)));
+    }
+  }
+
+  /* build array of transfer private keys */
+  GNUNET_assert (NULL != (transfer_privs = json_array ()));
+  for (unsigned int j = 0; j<TALER_CNC_KAPPA; j++)
+  {
+    if (j == noreveal_index)
+    {
+      /* This is crucial: exclude the transfer key for the noreval index! */
+      continue;
+    }
+    GNUNET_assert (0 ==
+                   json_array_append_new (transfer_privs,
+                                          GNUNET_JSON_from_data_auto (
+                                            &md.transfer_priv[j])));
+  }
+
+  /* build array of old age commitment, if applicable */
+  if (NULL != rd->melt_age_commitment_proof)
+  {
+    GNUNET_assert (NULL != rd->melt_h_age_commitment);
+    GNUNET_assert (NULL != (old_age_commitment = json_array ()));
+
+    for (size_t i = 0; i < rd->melt_age_commitment_proof->commitment.num; i++)
+    {
+      enum GNUNET_GenericReturnValue ret;
+      ret = json_array_append_new (
+        old_age_commitment,
+        GNUNET_JSON_from_data_auto (
+          &rd->melt_age_commitment_proof->commitment.keys[i]));
+      GNUNET_assert (0 == ret);
+    }
+  }
+
+  /* build main JSON request */
+  reveal_obj = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_data_auto ("transfer_pub",
+                                &md.transfer_pub[noreveal_index]),
+    GNUNET_JSON_pack_allow_null (
+      send_rms
+      ? GNUNET_JSON_pack_data_auto ("rms",
+                                    rms)
+      : GNUNET_JSON_pack_string ("rms",
+                                 NULL)),
+    GNUNET_JSON_pack_allow_null (
+      GNUNET_JSON_pack_array_steal ("old_age_commitment",
+                                    old_age_commitment)),
+    GNUNET_JSON_pack_array_steal ("transfer_privs",
+                                  transfer_privs),
+    GNUNET_JSON_pack_array_steal ("link_sigs",
+                                  link_sigs),
+    GNUNET_JSON_pack_array_steal ("new_denoms_h",
+                                  new_denoms_h),
+    GNUNET_JSON_pack_array_steal ("coin_evs",
+                                  coin_evs));
+  {
+    char pub_str[sizeof (struct TALER_RefreshCommitmentP) * 2];
+    char *end;
+
+    end = GNUNET_STRINGS_data_to_string (&md.rc,
+                                         sizeof (md.rc),
+                                         pub_str,
+                                         sizeof (pub_str));
+    *end = '\0';
+    GNUNET_snprintf (arg_str,
+                     sizeof (arg_str),
+                     "refreshes/%s/reveal",
+                     pub_str);
+  }
+  /* finally, we can actually issue the request */
+  rrh = GNUNET_new (struct TALER_EXCHANGE_RefreshesRevealHandle);
+  rrh->noreveal_index = noreveal_index;
+  rrh->reveal_cb = reveal_cb;
+  rrh->reveal_cb_cls = reveal_cb_cls;
+  rrh->md = md;
+  rrh->alg_values
+    = GNUNET_memdup (alg_values,
+                     md.num_fresh_coins
+                     * sizeof (struct TALER_ExchangeWithdrawValues));
+  rrh->url = TALER_url_join (url,
+                             arg_str,
+                             NULL);
+  if (NULL == rrh->url)
+  {
+    json_decref (reveal_obj);
+    TALER_EXCHANGE_free_melt_data_ (&md);
+    GNUNET_free (rrh->alg_values);
+    GNUNET_free (rrh);
+    return NULL;
+  }
+
+  eh = TALER_EXCHANGE_curl_easy_get_ (rrh->url);
+  if ( (NULL == eh) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&rrh->ctx,
+                              eh,
+                              reveal_obj)) )
+  {
+    GNUNET_break (0);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    json_decref (reveal_obj);
+    TALER_EXCHANGE_free_melt_data_ (&md);
+    GNUNET_free (rrh->alg_values);
+    GNUNET_free (rrh->url);
+    GNUNET_free (rrh);
+    return NULL;
+  }
+  json_decref (reveal_obj);
+  rrh->job = GNUNET_CURL_job_add2 (ctx,
+                                   eh,
+                                   rrh->ctx.headers,
+                                   &handle_refresh_reveal_finished,
+                                   rrh);
+  return rrh;
+}
+
+
+void
+TALER_EXCHANGE_refreshes_reveal_cancel (
+  struct TALER_EXCHANGE_RefreshesRevealHandle *rrh)
+{
+  if (NULL != rrh->job)
+  {
+    GNUNET_CURL_job_cancel (rrh->job);
+    rrh->job = NULL;
+  }
+  GNUNET_free (rrh->alg_values);
+  GNUNET_free (rrh->url);
+  TALER_curl_easy_post_finished (&rrh->ctx);
+  TALER_EXCHANGE_free_melt_data_ (&rrh->md);
+  GNUNET_free (rrh);
+}
+
+
+/* exchange_api_refreshes_reveal.c */
diff --git a/src/lib/exchange_api_refund.c b/src/lib/exchange_api_refund.c
new file mode 100644
index 00000000..7401bfe4
--- /dev/null
+++ b/src/lib/exchange_api_refund.c
@@ -0,0 +1,480 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2014-2023 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_refund.c
+ * @brief Implementation of the /refund request of the exchange's HTTP API
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <jansson.h>
+#include <microhttpd.h> /* just for HTTP status codes */
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_json_lib.h"
+#include "taler_exchange_service.h"
+#include "exchange_api_handle.h"
+#include "taler_signatures.h"
+#include "exchange_api_curl_defaults.h"
+
+
+/**
+ * @brief A Refund Handle
+ */
+struct TALER_EXCHANGE_RefundHandle
+{
+
+  /**
+   * The keys of the exchange this request handle will use
+   */
+  struct TALER_EXCHANGE_Keys *keys;
+
+  /**
+   * The url for this request.
+   */
+  char *url;
+
+  /**
+   * Context for #TEH_curl_easy_post(). Keeps the data that must
+   * persist for Curl to make the upload.
+   */
+  struct TALER_CURL_PostContext ctx;
+
+  /**
+   * Handle for the request.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Function to call with the result.
+   */
+  TALER_EXCHANGE_RefundCallback cb;
+
+  /**
+   * Closure for @a cb.
+   */
+  void *cb_cls;
+
+  /**
+   * Hash over the proposal data to identify the contract
+   * which is being refunded.
+   */
+  struct TALER_PrivateContractHashP h_contract_terms;
+
+  /**
+   * The coin's public key.  This is the value that must have been
+   * signed (blindly) by the Exchange.
+   */
+  struct TALER_CoinSpendPublicKeyP coin_pub;
+
+  /**
+   * The Merchant's public key.  Allows the merchant to later refund
+   * the transaction or to inquire about the wire transfer identifier.
+   */
+  struct TALER_MerchantPublicKeyP merchant;
+
+  /**
+   * Merchant-generated transaction ID for the refund.
+   */
+  uint64_t rtransaction_id;
+
+  /**
+   * Amount to be refunded, including refund fee charged by the
+   * exchange to the customer.
+   */
+  struct TALER_Amount refund_amount;
+
+};
+
+
+/**
+ * Verify that the signature on the "200 OK" response
+ * from the exchange is valid.
+ *
+ * @param[in,out] rh refund handle (refund fee added)
+ * @param json json reply with the signature
+ * @param[out] exchange_pub set to the exchange's public key
+ * @param[out] exchange_sig set to the exchange's signature
+ * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR if not
+ */
+static enum GNUNET_GenericReturnValue
+verify_refund_signature_ok (struct TALER_EXCHANGE_RefundHandle *rh,
+                            const json_t *json,
+                            struct TALER_ExchangePublicKeyP *exchange_pub,
+                            struct TALER_ExchangeSignatureP *exchange_sig)
+{
+  struct GNUNET_JSON_Specification spec[] = {
+    GNUNET_JSON_spec_fixed_auto ("exchange_sig",
+                                 exchange_sig),
+    GNUNET_JSON_spec_fixed_auto ("exchange_pub",
+                                 exchange_pub),
+    GNUNET_JSON_spec_end ()
+  };
+
+  if (GNUNET_OK !=
+      GNUNET_JSON_parse (json,
+                         spec,
+                         NULL, NULL))
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+  if (GNUNET_OK !=
+      TALER_EXCHANGE_test_signing_key (rh->keys,
+                                       exchange_pub))
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+  if (GNUNET_OK !=
+      TALER_exchange_online_refund_confirmation_verify (
+        &rh->h_contract_terms,
+        &rh->coin_pub,
+        &rh->merchant,
+        rh->rtransaction_id,
+        &rh->refund_amount,
+        exchange_pub,
+        exchange_sig))
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+  return GNUNET_OK;
+}
+
+
+/**
+ * Verify that the information on the "412 Dependency Failed" response
+ * from the exchange is valid and indeed shows that there is a refund
+ * transaction ID reuse going on.
+ *
+ * @param[in,out] rh refund handle (refund fee added)
+ * @param json json reply with the signature
+ * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR if not
+ */
+static enum GNUNET_GenericReturnValue
+verify_failed_dependency_ok (struct TALER_EXCHANGE_RefundHandle *rh,
+                             const json_t *json)
+{
+  const json_t *h;
+  json_t *e;
+  struct GNUNET_JSON_Specification spec[] = {
+    GNUNET_JSON_spec_array_const ("history",
+                                  &h),
+    GNUNET_JSON_spec_end ()
+  };
+
+  if (GNUNET_OK !=
+      GNUNET_JSON_parse (json,
+                         spec,
+                         NULL, NULL))
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+  if (1 != json_array_size (h))
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+  e = json_array_get (h, 0);
+  {
+    struct TALER_Amount amount;
+    const char *type;
+    struct TALER_MerchantSignatureP sig;
+    struct TALER_Amount refund_fee;
+    struct TALER_PrivateContractHashP h_contract_terms;
+    uint64_t rtransaction_id;
+    struct TALER_MerchantPublicKeyP merchant_pub;
+    struct GNUNET_JSON_Specification ispec[] = {
+      TALER_JSON_spec_amount_any ("amount",
+                                  &amount),
+      GNUNET_JSON_spec_string ("type",
+                               &type),
+      TALER_JSON_spec_amount_any ("refund_fee",
+                                  &refund_fee),
+      GNUNET_JSON_spec_fixed_auto ("merchant_sig",
+                                   &sig),
+      GNUNET_JSON_spec_fixed_auto ("h_contract_terms",
+                                   &h_contract_terms),
+      GNUNET_JSON_spec_fixed_auto ("merchant_pub",
+                                   &merchant_pub),
+      GNUNET_JSON_spec_uint64 ("rtransaction_id",
+                               &rtransaction_id),
+      GNUNET_JSON_spec_end ()
+    };
+
+    if (GNUNET_OK !=
+        GNUNET_JSON_parse (e,
+                           ispec,
+                           NULL, NULL))
+    {
+      GNUNET_break_op (0);
+      return GNUNET_SYSERR;
+    }
+    if (GNUNET_OK !=
+        TALER_merchant_refund_verify (&rh->coin_pub,
+                                      &h_contract_terms,
+                                      rtransaction_id,
+                                      &amount,
+                                      &merchant_pub,
+                                      &sig))
+    {
+      GNUNET_break_op (0);
+      return GNUNET_SYSERR;
+    }
+    if ( (rtransaction_id != rh->rtransaction_id) ||
+         (0 != GNUNET_memcmp (&rh->h_contract_terms,
+                              &h_contract_terms)) ||
+         (0 != GNUNET_memcmp (&rh->merchant,
+                              &merchant_pub)) ||
+         (0 == TALER_amount_cmp (&rh->refund_amount,
+                                 &amount)) )
+    {
+      GNUNET_break_op (0);
+      return GNUNET_SYSERR;
+    }
+  }
+  return GNUNET_OK;
+}
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /refund request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_RefundHandle`
+ * @param response_code HTTP response code, 0 on error
+ * @param response parsed JSON result, NULL on error
+ */
+static void
+handle_refund_finished (void *cls,
+                        long response_code,
+                        const void *response)
+{
+  struct TALER_EXCHANGE_RefundHandle *rh = cls;
+  const json_t *j = response;
+  struct TALER_EXCHANGE_RefundResponse rr = {
+    .hr.reply = j,
+    .hr.http_status = (unsigned int) response_code
+  };
+
+  rh->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    rr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    break;
+  case MHD_HTTP_OK:
+    if (GNUNET_OK !=
+        verify_refund_signature_ok (rh,
+                                    j,
+                                    &rr.details.ok.exchange_pub,
+                                    &rr.details.ok.exchange_sig))
+    {
+      GNUNET_break_op (0);
+      rr.hr.http_status = 0;
+      rr.hr.ec = TALER_EC_EXCHANGE_REFUND_INVALID_SIGNATURE_BY_EXCHANGE;
+    }
+    break;
+  case MHD_HTTP_BAD_REQUEST:
+    /* This should never happen, either us or the exchange is buggy
+       (or API version conflict); also can happen if the currency
+       differs (which we should obviously never support).
+       Just pass JSON reply to the application */
+    rr.hr.ec = TALER_JSON_get_error_code (j);
+    rr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    /* Nothing really to verify, exchange says one of the signatures is
+       invalid; as we checked them, this should never happen, we
+       should pass the JSON reply to the application */
+    rr.hr.ec = TALER_JSON_get_error_code (j);
+    rr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    /* Nothing really to verify, this should never
+       happen, we should pass the JSON reply to the application */
+    rr.hr.ec = TALER_JSON_get_error_code (j);
+    rr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_CONFLICT:
+    /* Requested total refunds exceed deposited amount */
+    rr.hr.ec = TALER_JSON_get_error_code (j);
+    rr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_GONE:
+    /* Kind of normal: the money was already sent to the merchant
+       (it was too late for the refund). */
+    rr.hr.ec = TALER_JSON_get_error_code (j);
+    rr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_PRECONDITION_FAILED:
+    if (GNUNET_OK !=
+        verify_failed_dependency_ok (rh,
+                                     j))
+    {
+      GNUNET_break (0);
+      rr.hr.http_status = 0;
+      rr.hr.ec = TALER_EC_EXCHANGE_REFUND_INVALID_FAILURE_PROOF_BY_EXCHANGE;
+      rr.hr.hint = "failed precondition proof returned by exchange is invalid";
+      break;
+    }
+    /* Two different refund requests were made about the same deposit, but
+       carrying identical refund transaction ids.  */
+    rr.hr.ec = TALER_JSON_get_error_code (j);
+    rr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  case MHD_HTTP_INTERNAL_SERVER_ERROR:
+    /* Server had an internal issue; we should retry, but this API
+       leaves this to the application */
+    rr.hr.ec = TALER_JSON_get_error_code (j);
+    rr.hr.hint = TALER_JSON_get_error_hint (j);
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    rr.hr.ec = TALER_JSON_get_error_code (j);
+    rr.hr.hint = TALER_JSON_get_error_hint (j);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange refund\n",
+                (unsigned int) response_code,
+                rr.hr.ec);
+    break;
+  }
+  rh->cb (rh->cb_cls,
+          &rr);
+  TALER_EXCHANGE_refund_cancel (rh);
+}
+
+
+struct TALER_EXCHANGE_RefundHandle *
+TALER_EXCHANGE_refund (
+  struct GNUNET_CURL_Context *ctx,
+  const char *url,
+  struct TALER_EXCHANGE_Keys *keys,
+  const struct TALER_Amount *amount,
+  const struct TALER_PrivateContractHashP *h_contract_terms,
+  const struct TALER_CoinSpendPublicKeyP *coin_pub,
+  uint64_t rtransaction_id,
+  const struct TALER_MerchantPrivateKeyP *merchant_priv,
+  TALER_EXCHANGE_RefundCallback cb,
+  void *cb_cls)
+{
+  struct TALER_MerchantPublicKeyP merchant_pub;
+  struct TALER_MerchantSignatureP merchant_sig;
+  struct TALER_EXCHANGE_RefundHandle *rh;
+  json_t *refund_obj;
+  CURL *eh;
+  char arg_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2 + 32];
+
+  GNUNET_CRYPTO_eddsa_key_get_public (&merchant_priv->eddsa_priv,
+                                      &merchant_pub.eddsa_pub);
+  TALER_merchant_refund_sign (coin_pub,
+                              h_contract_terms,
+                              rtransaction_id,
+                              amount,
+                              merchant_priv,
+                              &merchant_sig);
+  {
+    char pub_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2];
+    char *end;
+
+    end = GNUNET_STRINGS_data_to_string (
+      coin_pub,
+      sizeof (struct TALER_CoinSpendPublicKeyP),
+      pub_str,
+      sizeof (pub_str));
+    *end = '\0';
+    GNUNET_snprintf (arg_str,
+                     sizeof (arg_str),
+                     "coins/%s/refund",
+                     pub_str);
+  }
+  refund_obj = GNUNET_JSON_PACK (
+    TALER_JSON_pack_amount ("refund_amount",
+                            amount),
+    GNUNET_JSON_pack_data_auto ("h_contract_terms",
+                                h_contract_terms),
+    GNUNET_JSON_pack_uint64 ("rtransaction_id",
+                             rtransaction_id),
+    GNUNET_JSON_pack_data_auto ("merchant_pub",
+                                &merchant_pub),
+    GNUNET_JSON_pack_data_auto ("merchant_sig",
+                                &merchant_sig));
+  rh = GNUNET_new (struct TALER_EXCHANGE_RefundHandle);
+  rh->cb = cb;
+  rh->cb_cls = cb_cls;
+  rh->url = TALER_url_join (url,
+                            arg_str,
+                            NULL);
+  if (NULL == rh->url)
+  {
+    json_decref (refund_obj);
+    GNUNET_free (rh);
+    return NULL;
+  }
+  rh->h_contract_terms = *h_contract_terms;
+  rh->coin_pub = *coin_pub;
+  rh->merchant = merchant_pub;
+  rh->rtransaction_id = rtransaction_id;
+  rh->refund_amount = *amount;
+  eh = TALER_EXCHANGE_curl_easy_get_ (rh->url);
+  if ( (NULL == eh) ||
+       (GNUNET_OK !=
+        TALER_curl_easy_post (&rh->ctx,
+                              eh,
+                              refund_obj)) )
+  {
+    GNUNET_break (0);
+    if (NULL != eh)
+      curl_easy_cleanup (eh);
+    json_decref (refund_obj);
+    GNUNET_free (rh->url);
+    GNUNET_free (rh);
+    return NULL;
+  }
+  json_decref (refund_obj);
+  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+              "URL for refund: `%s'\n",
+              rh->url);
+  rh->keys = TALER_EXCHANGE_keys_incref (keys);
+  rh->job = GNUNET_CURL_job_add2 (ctx,
+                                  eh,
+                                  rh->ctx.headers,
+                                  &handle_refund_finished,
+                                  rh);
+  return rh;
+}
+
+
+void
+TALER_EXCHANGE_refund_cancel (struct TALER_EXCHANGE_RefundHandle *refund)
+{
+  if (NULL != refund->job)
+  {
+    GNUNET_CURL_job_cancel (refund->job);
+    refund->job = NULL;
+  }
+  GNUNET_free (refund->url);
+  TALER_curl_easy_post_finished (&refund->ctx);
+  TALER_EXCHANGE_keys_decref (refund->keys);
+  GNUNET_free (refund);
+}
+
+
+/* end of exchange_api_refund.c */
diff --git a/src/lib/exchange_api_stefan.c b/src/lib/exchange_api_stefan.c
new file mode 100644
index 00000000..c3576cd7
--- /dev/null
+++ b/src/lib/exchange_api_stefan.c
@@ -0,0 +1,320 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2023 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_stefan.c
+ * @brief calculations on the STEFAN curve
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "taler_json_lib.h"
+#include <gnunet/gnunet_curl_lib.h>
+#include "exchange_api_handle.h"
+#include <math.h>
+
+
+/**
+ * Determine smallest denomination in @a keys.
+ *
+ * @param keys exchange response to evaluate
+ * @return NULL on error (no denominations)
+ */
+static const struct TALER_Amount *
+get_unit (const struct TALER_EXCHANGE_Keys *keys)
+{
+  const struct TALER_Amount *min = NULL;
+
+  for (unsigned int i = 0; i<keys->num_denom_keys; i++)
+  {
+    const struct TALER_EXCHANGE_DenomPublicKey *dk
+      = &keys->denom_keys[i];
+
+    if ( (NULL == min) ||
+         (1 == TALER_amount_cmp (min,
+                                 /* > */
+                                 &dk->value)) )
+      min = &dk->value;
+  }
+  GNUNET_break (NULL != min);
+  return min;
+}
+
+
+/**
+ * Convert amount to double for STEFAN curve evaluation.
+ *
+ * @param a input amount
+ * @return (rounded) amount as a double
+ */
+static double
+amount_to_double (const struct TALER_Amount *a)
+{
+  double d = (double) a->value;
+
+  d += a->fraction / ((double) TALER_AMOUNT_FRAC_BASE);
+  return d;
+}
+
+
+/**
+ * Convert double to amount for STEFAN curve evaluation.
+ *
+ * @param dv input amount
+ * @param currency deisred currency
+ * @param[out] rval (rounded) amount as a double
+ */
+static void
+double_to_amount (double dv,
+                  const char *currency,
+                  struct TALER_Amount *rval)
+{
+  double rem;
+
+  GNUNET_assert (GNUNET_OK ==
+                 TALER_amount_set_zero (currency,
+                                        rval));
+  rval->value = floorl (dv);
+  rem = dv - ((double) rval->value);
+  if (rem < 0.0)
+    rem = 0.0;
+  rem *= TALER_AMOUNT_FRAC_BASE;
+  rval->fraction = floorl (rem);
+  if (rval->fraction >= TALER_AMOUNT_FRAC_BASE)
+  {
+    /* Strange, multiplication overflowed our range,
+       round up value instead */
+    rval->fraction = 0;
+    rval->value += 1;
+  }
+}
+
+
+enum GNUNET_GenericReturnValue
+TALER_EXCHANGE_keys_stefan_b2n (
+  const struct TALER_EXCHANGE_Keys *keys,
+  const struct TALER_Amount *brut,
+  struct TALER_Amount *net)
+{
+  const struct TALER_Amount *min;
+  double log_d = amount_to_double (&keys->stefan_log);
+  double lin_d = keys->stefan_lin;
+  double abs_d = amount_to_double (&keys->stefan_abs);
+  double bru_d = amount_to_double (brut);
+  double min_d;
+  double fee_d;
+  double net_d;
+
+  if (TALER_amount_is_zero (brut))
+  {
+    *net = *brut;
+    return GNUNET_NO;
+  }
+  min = get_unit (keys);
+  if (NULL == min)
+    return GNUNET_SYSERR;
+  if (1.0f <= keys->stefan_lin)
+  {
+    /* This cannot work, linear STEFAN fee estimate always
+       exceed any gross amount. */
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+  min_d = amount_to_double (min);
+  fee_d = abs_d
+          + log_d * log2 (bru_d / min_d)
+          + lin_d * bru_d;
+  if (fee_d > bru_d)
+  {
+    GNUNET_assert (GNUNET_OK ==
+                   TALER_amount_set_zero (brut->currency,
+                                          net));
+    return GNUNET_NO;
+  }
+  net_d = bru_d - fee_d;
+  double_to_amount (net_d,
+                    brut->currency,
+                    net);
+  return GNUNET_OK;
+}
+
+
+/**
+ * Our function
+ * f(x) := ne + ab + lo * log2(x/mi) + li * x - x
+ * for #newton().
+ */
+static double
+eval_f (double mi,
+        double ab,
+        double lo,
+        double li,
+        double ne,
+        double x)
+{
+  return ne + ab + lo * log2 (x / mi) + li * x - x;
+}
+
+
+/**
+ * Our function
+ * f'(x) := lo / log(2) / x + li - 1
+ * for #newton().
+ */
+static double
+eval_fp (double mi,
+         double lo,
+         double li,
+         double ne,
+         double x)
+{
+  return lo / log (2) / x + li - 1;
+}
+
+
+/**
+ * Use Newton's method to find x where f(x)=0.
+ *
+ * @return x where "eval_f(x)==0".
+ */
+static double
+newton (double mi,
+        double ab,
+        double lo,
+        double li,
+        double ne)
+{
+  const double eps = 0.00000001; /* max error allowed */
+  double min_ab = ne + ab; /* result cannot be smaller than this! */
+  /* compute lower bounds by various heuristics */
+  double min_ab_li = min_ab + min_ab * li;
+  double min_ab_li_lo = min_ab_li + log2 (min_ab_li / mi) * lo;
+  double min_ab_lo = min_ab + log2 (min_ab / mi) * lo;
+  double min_ab_lo_li = min_ab_lo + min_ab_lo * li;
+  /* take global lower bound */
+  double x_min = GNUNET_MAX (min_ab_lo_li,
+                             min_ab_li_lo);
+  double x = x_min; /* use lower bound as starting point */
+
+  /* Objective: invert
+     ne := br - ab - lo * log2 (br/mi) - li * br
+     to find 'br'.
+     Method: use Newton's method to find root of:
+     f(x) := ne + ab + lo * log2 (x/mi) + li * x - x
+     using also
+     f'(x) := lo / log(2) / x  + li - 1
+  */
+  /* Loop to abort in case of divergence;
+     100 is already very high, 2-4 is normal! */
+  for (unsigned int i = 0; i<100; i++)
+  {
+    double fx = eval_f (mi, ab, lo, li, ne, x);
+    double fxp = eval_fp (mi, lo, li, ne, x);
+    double x_new = x - fx / fxp;
+
+    if (fabs (x - x_new) <= eps)
+    {
+      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+                  "Needed %u rounds from %f to result BRUT %f => NET: %f\n",
+                  i,
+                  x_min,
+                  x_new,
+                  x_new - ab - li * x_new - lo * log2 (x / mi));
+      return x_new;
+    }
+    if (x_new < x_min)
+    {
+      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+                  "Divergence, obtained very bad estimate %f after %u 
rounds!\n",
+                  x_new,
+                  i);
+      return x_min;
+    }
+    x = x_new;
+  }
+  GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+              "Slow convergence, returning bad estimate %f!\n",
+              x);
+  return x;
+}
+
+
+enum GNUNET_GenericReturnValue
+TALER_EXCHANGE_keys_stefan_n2b (
+  const struct TALER_EXCHANGE_Keys *keys,
+  const struct TALER_Amount *net,
+  struct TALER_Amount *brut)
+{
+  const struct TALER_Amount *min;
+  double lin_d = keys->stefan_lin;
+  double log_d = amount_to_double (&keys->stefan_log);
+  double abs_d = amount_to_double (&keys->stefan_abs);
+  double net_d = amount_to_double (net);
+  double min_d;
+  double brut_d;
+
+  if (TALER_amount_is_zero (net))
+  {
+    *brut = *net;
+    return GNUNET_NO;
+  }
+  min = get_unit (keys);
+  if (NULL == min)
+    return GNUNET_SYSERR;
+  if (1.0f <= keys->stefan_lin)
+  {
+    /* This cannot work, linear STEFAN fee estimate always
+       exceed any gross amount. */
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+  min_d = amount_to_double (min);
+  brut_d = newton (min_d,
+                   abs_d,
+                   log_d,
+                   lin_d,
+                   net_d);
+  double_to_amount (brut_d,
+                    net->currency,
+                    brut);
+  return GNUNET_OK;
+}
+
+
+void
+TALER_EXCHANGE_keys_stefan_round (
+  const struct TALER_EXCHANGE_Keys *keys,
+  struct TALER_Amount *val)
+{
+  const struct TALER_Amount *min;
+  uint32_t mod = 1;
+  uint32_t frac;
+  uint32_t rst;
+
+  min = get_unit (keys);
+  if (NULL == min)
+    return;
+  frac = min->fraction;
+  while (0 == frac % 10)
+  {
+    mod *= 10;
+    frac /= 10;
+  }
+  rst = val->fraction % mod;
+  if (rst < mod / 2)
+    val->fraction -= rst;
+  else
+    val->fraction += mod - rst;
+}
diff --git a/src/lib/test_stefan.c b/src/lib/test_stefan.c
new file mode 100644
index 00000000..838cca76
--- /dev/null
+++ b/src/lib/test_stefan.c
@@ -0,0 +1,206 @@
+/*
+  This file is part of TALER
+  Copyright (C) 2023 Taler Systems SA
+
+  TALER is free software; you can redistribute it and/or modify it under the
+  terms of the GNU General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/test_stefan.c
+ * @brief test calculations on the STEFAN curve
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "taler_json_lib.h"
+#include <gnunet/gnunet_curl_lib.h>
+#include "exchange_api_handle.h"
+
+
+/**
+ * Check if @a a and @a b are numerically close.
+ *
+ * @param a an amount
+ * @param b an amount
+ * @return true if both values are quite close
+ */
+static bool
+amount_close (const struct TALER_Amount *a,
+              const struct TALER_Amount *b)
+{
+  struct TALER_Amount delta;
+
+  switch (TALER_amount_cmp (a,
+                            b))
+  {
+  case -1: /* a < b */
+    GNUNET_assert (0 <
+                   TALER_amount_subtract (&delta,
+                                          b,
+                                          a));
+    break;
+  case 0:
+    /* perfect */
+    return true;
+  case 1: /* a > b */
+    GNUNET_assert (0 <
+                   TALER_amount_subtract (&delta,
+                                          a,
+                                          b));
+    break;
+  }
+  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+              "Rounding error is %s\n",
+              TALER_amount2s (&delta));
+  if (delta.value > 0)
+  {
+    GNUNET_break (0);
+    return false;
+  }
+  if (delta.fraction > 5000)
+  {
+    GNUNET_break (0);
+    return false;
+  }
+  return true; /* let's consider this a rounding error */
+}
+
+
+int
+main (int argc,
+      char **argv)
+{
+  struct TALER_EXCHANGE_DenomPublicKey dk;
+  struct TALER_EXCHANGE_Keys keys = {
+    .denom_keys = &dk,
+    .num_denom_keys = 1
+  };
+  struct TALER_Amount brut;
+  struct TALER_Amount net;
+
+  (void) argc;
+  (void) argv;
+  GNUNET_log_setup ("test-stefan",
+                    "INFO",
+                    NULL);
+  GNUNET_assert (GNUNET_OK ==
+                 TALER_string_to_amount ("MAGIC:0.13",
+                                         &dk.value));
+  GNUNET_assert (GNUNET_OK ==
+                 TALER_string_to_amount ("MAGIC:1",
+                                         &keys.stefan_abs));
+  GNUNET_assert (GNUNET_OK ==
+                 TALER_string_to_amount ("MAGIC:0.13",
+                                         &keys.stefan_log));
+  keys.stefan_lin = 1.15;
+  GNUNET_assert (GNUNET_OK ==
+                 TALER_string_to_amount ("MAGIC:4",
+                                         &brut));
+  GNUNET_log_skip (1,
+                   GNUNET_NO);
+  GNUNET_assert (GNUNET_SYSERR ==
+                 TALER_EXCHANGE_keys_stefan_b2n (&keys,
+                                                 &brut,
+                                                 &net));
+  GNUNET_assert (GNUNET_OK ==
+                 TALER_string_to_amount ("MAGIC:4",
+                                         &net));
+  GNUNET_log_skip (1,
+                   GNUNET_NO);
+  GNUNET_assert (GNUNET_SYSERR ==
+                 TALER_EXCHANGE_keys_stefan_n2b (&keys,
+                                                 &net,
+                                                 &brut));
+  keys.stefan_lin = 1.0;
+  GNUNET_assert (GNUNET_OK ==
+                 TALER_string_to_amount ("MAGIC:4",
+                                         &brut));
+  GNUNET_log_skip (1,
+                   GNUNET_NO);
+  GNUNET_assert (GNUNET_SYSERR ==
+                 TALER_EXCHANGE_keys_stefan_b2n (&keys,
+                                                 &brut,
+                                                 &net));
+  GNUNET_assert (GNUNET_OK ==
+                 TALER_string_to_amount ("MAGIC:4",
+                                         &net));
+  GNUNET_log_skip (1,
+                   GNUNET_NO);
+  GNUNET_assert (GNUNET_SYSERR ==
+                 TALER_EXCHANGE_keys_stefan_n2b (&keys,
+                                                 &net,
+                                                 &brut));
+  GNUNET_assert (0 == GNUNET_get_log_skip ());
+  keys.stefan_lin = 0.1;
+
+  /* try various values for lin and log STEFAN values */
+  for (unsigned int li = 1; li < 13; li += 1)
+  {
+    keys.stefan_lin = 1.0 * li / 100.0;
+
+    for (unsigned int lx = 1; lx < 100; lx += 1)
+    {
+      keys.stefan_log.fraction = lx * TALER_AMOUNT_FRAC_BASE / 100;
+
+      /* Check brutto-to-netto is stable */
+      for (unsigned int i = 0; i<10; i++)
+      {
+        struct TALER_Amount rval;
+
+        brut.value = i;
+        brut.fraction = i * TALER_AMOUNT_FRAC_BASE / 10;
+        GNUNET_assert (GNUNET_SYSERR !=
+                       TALER_EXCHANGE_keys_stefan_b2n (&keys,
+                                                       &brut,
+                                                       &net));
+        GNUNET_assert (GNUNET_SYSERR !=
+                       TALER_EXCHANGE_keys_stefan_n2b (&keys,
+                                                       &net,
+                                                       &rval));
+        if (TALER_amount_is_zero (&net))
+          GNUNET_assert (TALER_amount_is_zero (&rval));
+        else
+        {
+          GNUNET_assert (amount_close (&brut,
+                                       &rval));
+          TALER_EXCHANGE_keys_stefan_round (&keys,
+                                            &rval);
+          GNUNET_assert (amount_close (&brut,
+                                       &rval));
+        }
+      }
+
+      /* Check netto-to-brutto is stable */
+      for (unsigned int i = 0; i<10; i++)
+      {
+        struct TALER_Amount rval;
+
+        net.value = i;
+        net.fraction = i * TALER_AMOUNT_FRAC_BASE / 10;
+        GNUNET_assert (GNUNET_SYSERR !=
+                       TALER_EXCHANGE_keys_stefan_n2b (&keys,
+                                                       &net,
+                                                       &brut));
+        GNUNET_assert (GNUNET_SYSERR !=
+                       TALER_EXCHANGE_keys_stefan_b2n (&keys,
+                                                       &brut,
+                                                       &rval));
+        GNUNET_assert (amount_close (&net,
+                                     &rval));
+        TALER_EXCHANGE_keys_stefan_round (&keys,
+                                          &rval);
+        GNUNET_assert (amount_close (&net,
+                                     &rval));
+      }
+    }
+  }
+  return 0;
+}

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]