Technical guide for adding a new Open Finance bank integration adapter (e.g., Itaú, Banco Inter, Mercado Pago) in FinanceHub (.NET 10), implementing IBankConnector, OAuth2/mTLS authentication, rate limiting, and publishing TransactionIngested events via MassTransit Outbox.
64
75%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./.agents/skills/openfinance-bank-adapter/SKILL.mdThis guide provides step-by-step instructions for implementing a new bank integration adapter in FinanceHub (.NET 10 / C# 13) compliant with Open Finance Brasil standards and FinanceHub microservice architecture rules.
Bank adapters in FinanceHub encapsulate all external bank communications within dedicated, isolated microservices (FinanceHub.ItauIntegration, FinanceHub.MercadoPagoIntegration, FinanceHub.InterIntegration). They translate proprietary or Open Finance API formats into standardized TransactionIngested integration events dispatched via MassTransit / RabbitMQ using the Transactional Outbox Pattern.
src/Services/FinanceHub.ItauIntegration/).IBankConnector and emit standard integration events (FinanceHub.Shared.Messaging).FinanceHub.Shared.Certificates.decimal with explicit ISO 4217 currency codes (BRL).IBankConnector InterfaceEvery adapter must implement the standard connector interface:
namespace FinanceHub.Shared.Connectors;
public interface IBankConnector
{
string BankIdentifier { get; }
Task<AuthTokenResponse> AuthenticateAsync(
BankCredentials credentials,
CancellationToken cancellationToken = default);
Task<IReadOnlyCollection<BankAccountDto>> GetAccountsAsync(
AuthTokenResponse token,
CancellationToken cancellationToken = default);
Task<IReadOnlyCollection<BankTransactionDto>> GetTransactionsAsync(
AuthTokenResponse token,
string accountId,
DateTimeOffset from,
DateTimeOffset to,
CancellationToken cancellationToken = default);
Task<HealthCheckResult> CheckHealthAsync(
CancellationToken cancellationToken = default);
}Scaffold the following folder structure inside the target bank integration service (e.g., src/Services/FinanceHub.ItauIntegration/):
src/Services/FinanceHub.<BankName>Integration/
├── Configuration/
│ └── <BankName>Options.cs
├── Security/
│ └── <BankName>AuthHandler.cs
├── Dtos/
│ ├── <BankName>AccountResponseDto.cs
│ ├── <BankName>TransactionResponseDto.cs
│ └── <BankName>TokenResponseDto.cs
├── Services/
│ ├── <BankName>MappingProfile.cs
│ └── <BankName>Connector.cs
├── Handlers/
│ └── FetchTransactionsCommandHandler.cs
└── Program.csCreate <BankName>Options.cs to hold API keys, client credentials, endpoints, and mTLS cert details:
namespace FinanceHub.<BankName>Integration.Configuration;
public sealed class <BankName>Options
{
public const string SectionName = "BankAdapters:<BankName>";
public string BaseUrl { get; set; } = string.Empty;
public string AuthEndpoint { get; set; } = string.Empty;
public string ClientId { get; set; } = string.Empty;
public string ClientSecret { get; set; } = string.Empty;
public string CertificateThumbprint { get; set; } = string.Empty;
public string Scope { get; set; } = "accounts transactions openid";
public int RateLimitPerMinute { get; set; } = 120;
}FinanceHub.Shared.Certificates)Open Finance integrations require mTLS with ICP-Brasil certificates (X509Certificate2) and OAuth2 Client Credentials (private_key_jwt).
Certificate Management:
Utilize FinanceHub.Shared.Certificates.CertificateLoader for retrieving and validating e-CNPJ client certificates.
Delegating Auth Handler (<BankName>AuthHandler.cs):
Implement a DelegatingHandler to automatically attach OAuth2 bearer tokens and manage auto-refresh before expiration.
public sealed class <BankName>AuthHandler : DelegatingHandler
{
private readonly IMemoryCache _cache;
private readonly IOptions<<BankName>Options> _options;
public <BankName>AuthHandler(IMemoryCache cache, IOptions<<BankName>Options> options)
{
_cache = cache;
_options = options;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var token = await _cache.GetOrCreateAsync($"token:<BankName>", async entry =>
{
var newToken = await FetchTokenAsync(cancellationToken);
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(newToken.ExpiresIn - 60);
return newToken.AccessToken;
});
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
return await base.SendAsync(request, cancellationToken);
}
}Use .NET 10 standard resilience handler with Microsoft.Extensions.Http.Resilience:
services.AddHttpClient<<BankName>Connector>(client =>
{
client.BaseAddress = new Uri(options.BaseUrl);
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
var handler = new HttpClientHandler();
var cert = CertificateLoader.LoadClientCertificate(options.CertificateThumbprint);
handler.ClientCertificates.Add(cert);
return handler;
})
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.Retry.Delay = TimeSpan.FromSeconds(2);
options.Retry.BackoffType = Polly.DelayBackoffType.Exponential;
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(10);
});Map external bank DTOs to TransactionIngested integration events (FinanceHub.Shared.Messaging):
public static class <BankName>MappingProfile
{
public static TransactionIngested ToIntegrationEvent(this <BankName>TransactionDto dto, string bankCode, string accountId)
{
return new TransactionIngested(
ExternalId: dto.TransactionId,
AccountId: accountId,
BankCode: bankCode,
Amount: decimal.Parse(dto.Amount, CultureInfo.InvariantCulture),
Currency: dto.CurrencyCode ?? "BRL",
TransactionDate: DateTimeOffset.Parse(dto.BookingDateTime),
Description: dto.TransactionInformation,
Type: dto.CreditDebitIndicator == "CRDT" ? "CREDIT" : "DEBIT",
IngestedAt: DateTimeOffset.UtcNow
);
}
}Create <BankName>Connector.cs implementing IBankConnector:
public sealed class <BankName>Connector : IBankConnector
{
private readonly HttpClient _httpClient;
private readonly ILogger<<BankName>Connector> _logger;
public string BankIdentifier => "<BankName>";
public <BankName>Connector(HttpClient httpClient, ILogger<<BankName>Connector> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<IReadOnlyCollection<BankTransactionDto>> GetTransactionsAsync(
AuthTokenResponse token,
string accountId,
DateTimeOffset from,
DateTimeOffset to,
CancellationToken cancellationToken = default)
{
var url = $"open-banking/v1/accounts/{accountId}/transactions?from={from:yyyy-MM-dd}&to={to:yyyy-MM-dd}";
var response = await _httpClient.GetFromJsonAsync<<BankName>TransactionResponseDto>(url, cancellationToken);
if (response?.Data == null) return Array.Empty<BankTransactionDto>();
return response.Data.Select(t => t.ToDomain()).ToList().AsReadOnly();
}
}appsettings.json contains BankAdapters:<BankName> section with valid secrets from Key Vault / KMS.FinanceHub.Shared.Certificates.tests/FinanceHub.<BankName>Integration.Tests/ asserting correct transaction DTO mapping, decimal precision, and resilience behavior.dotnet build and dotnet test.a8dbf2a
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.